From 550982430f357233837076701be1b4014e745895 Mon Sep 17 00:00:00 2001
From: Dean Sheather
Date: Wed, 8 Feb 2023 20:00:21 +0000
Subject: [PATCH 01/24] feat: add template max_ttl
---
coderd/activitybump.go | 4 ++
coderd/database/dump.sql | 6 +-
...0097_workspace_build_max_deadline.down.sql | 3 +
...000097_workspace_build_max_deadline.up.sql | 3 +
coderd/database/models.go | 4 +-
coderd/database/queries.sql.go | 62 +++++++++++++------
coderd/database/queries/workspacebuilds.sql | 6 +-
.../provisionerdserver/provisionerdserver.go | 56 ++++++++++++++---
coderd/workspaces.go | 6 ++
9 files changed, 118 insertions(+), 32 deletions(-)
create mode 100644 coderd/database/migrations/000097_workspace_build_max_deadline.down.sql
create mode 100644 coderd/database/migrations/000097_workspace_build_max_deadline.up.sql
diff --git a/coderd/activitybump.go b/coderd/activitybump.go
index 059655ed8f33e..744393e8b01de 100644
--- a/coderd/activitybump.go
+++ b/coderd/activitybump.go
@@ -70,12 +70,16 @@ func activityBumpWorkspace(log slog.Logger, db database.Store, workspaceID uuid.
}
newDeadline := database.Now().Add(bumpAmount)
+ if !build.MaxDeadline.IsZero() && newDeadline.After(build.MaxDeadline) {
+ newDeadline = build.MaxDeadline
+ }
if _, err := s.UpdateWorkspaceBuildByID(ctx, database.UpdateWorkspaceBuildByIDParams{
ID: build.ID,
UpdatedAt: database.Now(),
ProvisionerState: build.ProvisionerState,
Deadline: newDeadline,
+ MaxDeadline: build.MaxDeadline,
}); err != nil {
return xerrors.Errorf("update workspace build: %w", err)
}
diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql
index e8bf641dd59e6..502ff5acd8cc1 100644
--- a/coderd/database/dump.sql
+++ b/coderd/database/dump.sql
@@ -410,7 +410,8 @@ CREATE TABLE templates (
user_acl jsonb DEFAULT '{}'::jsonb NOT NULL,
group_acl jsonb DEFAULT '{}'::jsonb NOT NULL,
display_name character varying(64) DEFAULT ''::character varying NOT NULL,
- allow_user_cancel_workspace_jobs boolean DEFAULT true NOT NULL
+ allow_user_cancel_workspace_jobs boolean DEFAULT true NOT NULL,
+ max_ttl bigint DEFAULT '0'::bigint NOT NULL
);
COMMENT ON COLUMN templates.default_ttl IS 'The default duration for auto-stop for workspaces created from this template.';
@@ -529,7 +530,8 @@ CREATE TABLE workspace_builds (
job_id uuid NOT NULL,
deadline timestamp with time zone DEFAULT '0001-01-01 00:00:00+00'::timestamp with time zone NOT NULL,
reason build_reason DEFAULT 'initiator'::build_reason NOT NULL,
- daily_cost integer DEFAULT 0 NOT NULL
+ daily_cost integer DEFAULT 0 NOT NULL,
+ max_deadline timestamp with time zone DEFAULT '0001-01-01 00:00:00+00'::timestamp with time zone NOT NULL
);
CREATE TABLE workspace_resource_metadata (
diff --git a/coderd/database/migrations/000097_workspace_build_max_deadline.down.sql b/coderd/database/migrations/000097_workspace_build_max_deadline.down.sql
new file mode 100644
index 0000000000000..2fecc6595bbc1
--- /dev/null
+++ b/coderd/database/migrations/000097_workspace_build_max_deadline.down.sql
@@ -0,0 +1,3 @@
+ALTER TABLE "workspace_builds" DROP COLUMN "max_deadline";
+
+ALTER TABLE "templates" DROP COLUMN "max_ttl";
diff --git a/coderd/database/migrations/000097_workspace_build_max_deadline.up.sql b/coderd/database/migrations/000097_workspace_build_max_deadline.up.sql
new file mode 100644
index 0000000000000..4a382fd9f107c
--- /dev/null
+++ b/coderd/database/migrations/000097_workspace_build_max_deadline.up.sql
@@ -0,0 +1,3 @@
+ALTER TABLE "templates" ADD COLUMN "max_ttl" bigint DEFAULT '0'::bigint NOT NULL;
+
+ALTER TABLE "workspace_builds" ADD COLUMN "max_deadline" timestamp with time zone DEFAULT '0001-01-01 00:00:00+00'::timestamp with time zone NOT NULL;
diff --git a/coderd/database/models.go b/coderd/database/models.go
index ce5d2204a7266..d8826a06bac2b 100644
--- a/coderd/database/models.go
+++ b/coderd/database/models.go
@@ -1417,7 +1417,8 @@ type Template struct {
// Display name is a custom, human-friendly template name that user can set.
DisplayName string `db:"display_name" json:"display_name"`
// Allow users to cancel in-progress workspace jobs.
- AllowUserCancelWorkspaceJobs bool `db:"allow_user_cancel_workspace_jobs" json:"allow_user_cancel_workspace_jobs"`
+ AllowUserCancelWorkspaceJobs bool `db:"allow_user_cancel_workspace_jobs" json:"allow_user_cancel_workspace_jobs"`
+ MaxTtl int64 `db:"max_ttl" json:"max_ttl"`
}
type TemplateVersion struct {
@@ -1567,6 +1568,7 @@ type WorkspaceBuild struct {
Deadline time.Time `db:"deadline" json:"deadline"`
Reason BuildReason `db:"reason" json:"reason"`
DailyCost int32 `db:"daily_cost" json:"daily_cost"`
+ MaxDeadline time.Time `db:"max_deadline" json:"max_deadline"`
}
type WorkspaceBuildParameter struct {
diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go
index 9ed8530997320..845fd933dd251 100644
--- a/coderd/database/queries.sql.go
+++ b/coderd/database/queries.sql.go
@@ -3088,7 +3088,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
+ 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
FROM
templates
WHERE
@@ -3117,13 +3117,14 @@ func (q *sqlQuerier) GetTemplateByID(ctx context.Context, id uuid.UUID) (Templat
&i.GroupACL,
&i.DisplayName,
&i.AllowUserCancelWorkspaceJobs,
+ &i.MaxTtl,
)
return i, err
}
const getTemplateByOrganizationAndName = `-- name: GetTemplateByOrganizationAndName :one
SELECT
- id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs
+ 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
FROM
templates
WHERE
@@ -3160,12 +3161,13 @@ func (q *sqlQuerier) GetTemplateByOrganizationAndName(ctx context.Context, arg G
&i.GroupACL,
&i.DisplayName,
&i.AllowUserCancelWorkspaceJobs,
+ &i.MaxTtl,
)
return i, err
}
const getTemplates = `-- name: GetTemplates :many
-SELECT id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs FROM templates
+SELECT id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl FROM templates
ORDER BY (name, id) ASC
`
@@ -3195,6 +3197,7 @@ func (q *sqlQuerier) GetTemplates(ctx context.Context) ([]Template, error) {
&i.GroupACL,
&i.DisplayName,
&i.AllowUserCancelWorkspaceJobs,
+ &i.MaxTtl,
); err != nil {
return nil, err
}
@@ -3211,7 +3214,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
+ 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
FROM
templates
WHERE
@@ -3278,6 +3281,7 @@ func (q *sqlQuerier) GetTemplatesWithFilter(ctx context.Context, arg GetTemplate
&i.GroupACL,
&i.DisplayName,
&i.AllowUserCancelWorkspaceJobs,
+ &i.MaxTtl,
); err != nil {
return nil, err
}
@@ -3312,7 +3316,7 @@ INSERT INTO
allow_user_cancel_workspace_jobs
)
VALUES
- ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) RETURNING id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs
+ ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) RETURNING id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl
`
type InsertTemplateParams struct {
@@ -3369,6 +3373,7 @@ func (q *sqlQuerier) InsertTemplate(ctx context.Context, arg InsertTemplateParam
&i.GroupACL,
&i.DisplayName,
&i.AllowUserCancelWorkspaceJobs,
+ &i.MaxTtl,
)
return i, err
}
@@ -3382,7 +3387,7 @@ SET
WHERE
id = $3
RETURNING
- id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs
+ 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
`
type UpdateTemplateACLByIDParams struct {
@@ -3411,6 +3416,7 @@ func (q *sqlQuerier) UpdateTemplateACLByID(ctx context.Context, arg UpdateTempla
&i.GroupACL,
&i.DisplayName,
&i.AllowUserCancelWorkspaceJobs,
+ &i.MaxTtl,
)
return i, err
}
@@ -3471,7 +3477,7 @@ SET
WHERE
id = $1
RETURNING
- id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs
+ 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
`
type UpdateTemplateMetaByIDParams struct {
@@ -3514,6 +3520,7 @@ func (q *sqlQuerier) UpdateTemplateMetaByID(ctx context.Context, arg UpdateTempl
&i.GroupACL,
&i.DisplayName,
&i.AllowUserCancelWorkspaceJobs,
+ &i.MaxTtl,
)
return i, err
}
@@ -5561,7 +5568,7 @@ func (q *sqlQuerier) InsertWorkspaceBuildParameters(ctx context.Context, arg Ins
const getLatestWorkspaceBuildByWorkspaceID = `-- name: GetLatestWorkspaceBuildByWorkspaceID :one
SELECT
- id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost
+ id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, max_deadline
FROM
workspace_builds
WHERE
@@ -5589,12 +5596,13 @@ func (q *sqlQuerier) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, w
&i.Deadline,
&i.Reason,
&i.DailyCost,
+ &i.MaxDeadline,
)
return i, err
}
const getLatestWorkspaceBuilds = `-- name: GetLatestWorkspaceBuilds :many
-SELECT wb.id, wb.created_at, wb.updated_at, wb.workspace_id, wb.template_version_id, wb.build_number, wb.transition, wb.initiator_id, wb.provisioner_state, wb.job_id, wb.deadline, wb.reason, wb.daily_cost
+SELECT wb.id, wb.created_at, wb.updated_at, wb.workspace_id, wb.template_version_id, wb.build_number, wb.transition, wb.initiator_id, wb.provisioner_state, wb.job_id, wb.deadline, wb.reason, wb.daily_cost, wb.max_deadline
FROM (
SELECT
workspace_id, MAX(build_number) as max_build_number
@@ -5631,6 +5639,7 @@ func (q *sqlQuerier) GetLatestWorkspaceBuilds(ctx context.Context) ([]WorkspaceB
&i.Deadline,
&i.Reason,
&i.DailyCost,
+ &i.MaxDeadline,
); err != nil {
return nil, err
}
@@ -5646,7 +5655,7 @@ func (q *sqlQuerier) GetLatestWorkspaceBuilds(ctx context.Context) ([]WorkspaceB
}
const getLatestWorkspaceBuildsByWorkspaceIDs = `-- name: GetLatestWorkspaceBuildsByWorkspaceIDs :many
-SELECT wb.id, wb.created_at, wb.updated_at, wb.workspace_id, wb.template_version_id, wb.build_number, wb.transition, wb.initiator_id, wb.provisioner_state, wb.job_id, wb.deadline, wb.reason, wb.daily_cost
+SELECT wb.id, wb.created_at, wb.updated_at, wb.workspace_id, wb.template_version_id, wb.build_number, wb.transition, wb.initiator_id, wb.provisioner_state, wb.job_id, wb.deadline, wb.reason, wb.daily_cost, wb.max_deadline
FROM (
SELECT
workspace_id, MAX(build_number) as max_build_number
@@ -5685,6 +5694,7 @@ func (q *sqlQuerier) GetLatestWorkspaceBuildsByWorkspaceIDs(ctx context.Context,
&i.Deadline,
&i.Reason,
&i.DailyCost,
+ &i.MaxDeadline,
); err != nil {
return nil, err
}
@@ -5701,7 +5711,7 @@ func (q *sqlQuerier) GetLatestWorkspaceBuildsByWorkspaceIDs(ctx context.Context,
const getWorkspaceBuildByID = `-- name: GetWorkspaceBuildByID :one
SELECT
- id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost
+ id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, max_deadline
FROM
workspace_builds
WHERE
@@ -5727,13 +5737,14 @@ func (q *sqlQuerier) GetWorkspaceBuildByID(ctx context.Context, id uuid.UUID) (W
&i.Deadline,
&i.Reason,
&i.DailyCost,
+ &i.MaxDeadline,
)
return i, err
}
const getWorkspaceBuildByJobID = `-- name: GetWorkspaceBuildByJobID :one
SELECT
- id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost
+ id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, max_deadline
FROM
workspace_builds
WHERE
@@ -5759,13 +5770,14 @@ func (q *sqlQuerier) GetWorkspaceBuildByJobID(ctx context.Context, jobID uuid.UU
&i.Deadline,
&i.Reason,
&i.DailyCost,
+ &i.MaxDeadline,
)
return i, err
}
const getWorkspaceBuildByWorkspaceIDAndBuildNumber = `-- name: GetWorkspaceBuildByWorkspaceIDAndBuildNumber :one
SELECT
- id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost
+ id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, max_deadline
FROM
workspace_builds
WHERE
@@ -5795,13 +5807,14 @@ func (q *sqlQuerier) GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx context.Co
&i.Deadline,
&i.Reason,
&i.DailyCost,
+ &i.MaxDeadline,
)
return i, err
}
const getWorkspaceBuildsByWorkspaceID = `-- name: GetWorkspaceBuildsByWorkspaceID :many
SELECT
- id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost
+ id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, max_deadline
FROM
workspace_builds
WHERE
@@ -5870,6 +5883,7 @@ func (q *sqlQuerier) GetWorkspaceBuildsByWorkspaceID(ctx context.Context, arg Ge
&i.Deadline,
&i.Reason,
&i.DailyCost,
+ &i.MaxDeadline,
); err != nil {
return nil, err
}
@@ -5885,7 +5899,7 @@ func (q *sqlQuerier) GetWorkspaceBuildsByWorkspaceID(ctx context.Context, arg Ge
}
const getWorkspaceBuildsCreatedAfter = `-- name: GetWorkspaceBuildsCreatedAfter :many
-SELECT id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost FROM workspace_builds WHERE created_at > $1
+SELECT id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, max_deadline FROM workspace_builds WHERE created_at > $1
`
func (q *sqlQuerier) GetWorkspaceBuildsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceBuild, error) {
@@ -5911,6 +5925,7 @@ func (q *sqlQuerier) GetWorkspaceBuildsCreatedAfter(ctx context.Context, created
&i.Deadline,
&i.Reason,
&i.DailyCost,
+ &i.MaxDeadline,
); err != nil {
return nil, err
}
@@ -5939,10 +5954,11 @@ INSERT INTO
job_id,
provisioner_state,
deadline,
+ max_deadline,
reason
)
VALUES
- ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost
+ ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, max_deadline
`
type InsertWorkspaceBuildParams struct {
@@ -5957,6 +5973,7 @@ type InsertWorkspaceBuildParams struct {
JobID uuid.UUID `db:"job_id" json:"job_id"`
ProvisionerState []byte `db:"provisioner_state" json:"provisioner_state"`
Deadline time.Time `db:"deadline" json:"deadline"`
+ MaxDeadline time.Time `db:"max_deadline" json:"max_deadline"`
Reason BuildReason `db:"reason" json:"reason"`
}
@@ -5973,6 +5990,7 @@ func (q *sqlQuerier) InsertWorkspaceBuild(ctx context.Context, arg InsertWorkspa
arg.JobID,
arg.ProvisionerState,
arg.Deadline,
+ arg.MaxDeadline,
arg.Reason,
)
var i WorkspaceBuild
@@ -5990,6 +6008,7 @@ func (q *sqlQuerier) InsertWorkspaceBuild(ctx context.Context, arg InsertWorkspa
&i.Deadline,
&i.Reason,
&i.DailyCost,
+ &i.MaxDeadline,
)
return i, err
}
@@ -6000,9 +6019,10 @@ UPDATE
SET
updated_at = $2,
provisioner_state = $3,
- deadline = $4
+ deadline = $4,
+ max_deadline = $5
WHERE
- id = $1 RETURNING id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost
+ id = $1 RETURNING id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, max_deadline
`
type UpdateWorkspaceBuildByIDParams struct {
@@ -6010,6 +6030,7 @@ type UpdateWorkspaceBuildByIDParams struct {
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
ProvisionerState []byte `db:"provisioner_state" json:"provisioner_state"`
Deadline time.Time `db:"deadline" json:"deadline"`
+ MaxDeadline time.Time `db:"max_deadline" json:"max_deadline"`
}
func (q *sqlQuerier) UpdateWorkspaceBuildByID(ctx context.Context, arg UpdateWorkspaceBuildByIDParams) (WorkspaceBuild, error) {
@@ -6018,6 +6039,7 @@ func (q *sqlQuerier) UpdateWorkspaceBuildByID(ctx context.Context, arg UpdateWor
arg.UpdatedAt,
arg.ProvisionerState,
arg.Deadline,
+ arg.MaxDeadline,
)
var i WorkspaceBuild
err := row.Scan(
@@ -6034,6 +6056,7 @@ func (q *sqlQuerier) UpdateWorkspaceBuildByID(ctx context.Context, arg UpdateWor
&i.Deadline,
&i.Reason,
&i.DailyCost,
+ &i.MaxDeadline,
)
return i, err
}
@@ -6044,7 +6067,7 @@ UPDATE
SET
daily_cost = $2
WHERE
- id = $1 RETURNING id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost
+ id = $1 RETURNING id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, provisioner_state, job_id, deadline, reason, daily_cost, max_deadline
`
type UpdateWorkspaceBuildCostByIDParams struct {
@@ -6069,6 +6092,7 @@ func (q *sqlQuerier) UpdateWorkspaceBuildCostByID(ctx context.Context, arg Updat
&i.Deadline,
&i.Reason,
&i.DailyCost,
+ &i.MaxDeadline,
)
return i, err
}
diff --git a/coderd/database/queries/workspacebuilds.sql b/coderd/database/queries/workspacebuilds.sql
index 30658634da4e0..b56be8f1d1de5 100644
--- a/coderd/database/queries/workspacebuilds.sql
+++ b/coderd/database/queries/workspacebuilds.sql
@@ -119,10 +119,11 @@ INSERT INTO
job_id,
provisioner_state,
deadline,
+ max_deadline,
reason
)
VALUES
- ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING *;
+ ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING *;
-- name: UpdateWorkspaceBuildByID :one
UPDATE
@@ -130,7 +131,8 @@ UPDATE
SET
updated_at = $2,
provisioner_state = $3,
- deadline = $4
+ deadline = $4,
+ max_deadline = $5
WHERE
id = $1 RETURNING *;
diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go
index 08c60d8b50f89..96d90c2dd85ac 100644
--- a/coderd/provisionerdserver/provisionerdserver.go
+++ b/coderd/provisionerdserver/provisionerdserver.go
@@ -517,15 +517,31 @@ func (server *Server) FailJob(ctx context.Context, failJob *proto.FailedJob) (*p
if err != nil {
return nil, xerrors.Errorf("unmarshal workspace provision input: %w", err)
}
- build, err := server.Database.UpdateWorkspaceBuildByID(ctx, database.UpdateWorkspaceBuildByIDParams{
- ID: input.WorkspaceBuildID,
- UpdatedAt: database.Now(),
- ProvisionerState: jobType.WorkspaceBuild.State,
- // We are explicitly not updating deadline here.
- })
+
+ var build database.WorkspaceBuild
+ err := server.Database.InTx(func(db database.Store) error {
+ workspaceBuild, err := server.Database.GetWorkspaceBuildByID(ctx, input.WorkspaceBuildID)
+ if err != nil {
+ return xerrors.Errorf("get workspace build: %w", err)
+ }
+
+ build, err = server.Database.UpdateWorkspaceBuildByID(ctx, database.UpdateWorkspaceBuildByIDParams{
+ ID: input.WorkspaceBuildID,
+ UpdatedAt: database.Now(),
+ ProvisionerState: jobType.WorkspaceBuild.State,
+ Deadline: workspaceBuild.Deadline,
+ MaxDeadline: workspaceBuild.MaxDeadline,
+ })
+ if err != nil {
+ return xerrors.Errorf("update workspace build state: %w", err)
+ }
+
+ return nil
+ }, nil)
if err != nil {
- return nil, xerrors.Errorf("update workspace build state: %w", err)
+ return nil, err
}
+
err = server.Pubsub.Publish(codersdk.WorkspaceNotifyChannel(build.WorkspaceID), []byte{})
if err != nil {
return nil, xerrors.Errorf("update workspace: %w", err)
@@ -709,8 +725,31 @@ func (server *Server) CompleteJob(ctx context.Context, completed *proto.Complete
} else {
// Huh? Did the workspace get deleted?
// In any case, since this is just for the TTL, try and continue anyway.
- server.Logger.Error(ctx, "fetch workspace for build", slog.F("workspace_build_id", workspaceBuild.ID), slog.F("workspace_id", workspaceBuild.WorkspaceID))
+ server.Logger.Error(ctx,
+ "fetch workspace for build",
+ slog.F("workspace_build_id", workspaceBuild.ID),
+ slog.F("workspace_id", workspaceBuild.WorkspaceID),
+ )
}
+
+ var workspaceMaxDeadline time.Time
+ template, err := db.GetTemplateByID(ctx, workspace.TemplateID)
+ if err == nil {
+ if template.MaxTtl > 0 {
+ workspaceMaxDeadline = now.Add(time.Duration(template.MaxTtl))
+ if !workspaceDeadline.IsZero() && workspaceMaxDeadline.Before(workspaceDeadline) {
+ workspaceDeadline = workspaceMaxDeadline
+ }
+ }
+ } else {
+ server.Logger.Error(ctx,
+ "fetch template for build",
+ slog.F("workspace_build_id", workspaceBuild.ID),
+ slog.F("workspace_id", workspaceBuild.WorkspaceID),
+ slog.F("template_id", workspace.TemplateID),
+ )
+ }
+
err = db.UpdateProvisionerJobWithCompleteByID(ctx, database.UpdateProvisionerJobWithCompleteByIDParams{
ID: jobID,
UpdatedAt: database.Now(),
@@ -725,6 +764,7 @@ func (server *Server) CompleteJob(ctx context.Context, completed *proto.Complete
_, err = db.UpdateWorkspaceBuildByID(ctx, database.UpdateWorkspaceBuildByIDParams{
ID: workspaceBuild.ID,
Deadline: workspaceDeadline,
+ MaxDeadline: workspaceMaxDeadline,
ProvisionerState: jobType.WorkspaceBuild.State,
UpdatedAt: now,
})
diff --git a/coderd/workspaces.go b/coderd/workspaces.go
index 972647a73df34..06190e5c4b5ed 100644
--- a/coderd/workspaces.go
+++ b/coderd/workspaces.go
@@ -902,12 +902,18 @@ func (api *API) putExtendWorkspace(rw http.ResponseWriter, r *http.Request) {
resp.Message = "Cannot extend workspace: " + err.Error()
return err
}
+ if !build.MaxDeadline.IsZero() && newDeadline.After(build.MaxDeadline) {
+ code = http.StatusBadRequest
+ resp.Message = "Cannot extend workspace beyond max deadline."
+ return xerrors.New("Cannot extend workspace: deadline is beyond max deadline imposed by template")
+ }
if _, err := s.UpdateWorkspaceBuildByID(ctx, database.UpdateWorkspaceBuildByIDParams{
ID: build.ID,
UpdatedAt: build.UpdatedAt,
ProvisionerState: build.ProvisionerState,
Deadline: newDeadline,
+ MaxDeadline: build.MaxDeadline,
}); err != nil {
code = http.StatusInternalServerError
resp.Message = "Failed to extend workspace deadline."
From 4c6a50169b329cf15d559be10870ea27886cb6f2 Mon Sep 17 00:00:00 2001
From: Dean Sheather
Date: Wed, 15 Feb 2023 11:31:38 +0000
Subject: [PATCH 02/24] chore: split enterprise code for max_ttl into interface
---
coderd/coderd.go | 48 +++++-----
coderd/database/models.go | 2 +-
coderd/database/queries.sql.go | 16 ++--
coderd/database/sqlc.yaml | 1 +
.../provisionerdserver/provisionerdserver.go | 89 +++++++++++--------
.../provisionerdserver_test.go | 78 ++++++++++------
.../templatescheduleoptions.go | 47 ++++++++++
coderd/templates.go | 4 +-
coderd/templates_test.go | 8 +-
codersdk/deployment.go | 2 +
enterprise/audit/table.go | 1 +
enterprise/coderd/coderd.go | 13 +++
enterprise/coderd/coderd_test.go | 1 +
enterprise/coderd/provisionerdaemons.go | 39 ++++++--
14 files changed, 240 insertions(+), 109 deletions(-)
create mode 100644 coderd/provisionerdserver/templatescheduleoptions.go
diff --git a/coderd/coderd.go b/coderd/coderd.go
index bfce5a5fb1a88..a9542f65e4ca1 100644
--- a/coderd/coderd.go
+++ b/coderd/coderd.go
@@ -110,12 +110,13 @@ type Options struct {
RealIPConfig *httpmw.RealIPConfig
TrialGenerator func(ctx context.Context, email string) error
// TLSCertificates is used to mesh DERP servers securely.
- TLSCertificates []tls.Certificate
- TailnetCoordinator tailnet.Coordinator
- DERPServer *derp.Server
- DERPMap *tailcfg.DERPMap
- SwaggerEndpoint bool
- SetUserGroups func(ctx context.Context, tx database.Store, userID uuid.UUID, groupNames []string) error
+ TLSCertificates []tls.Certificate
+ TailnetCoordinator tailnet.Coordinator
+ DERPServer *derp.Server
+ DERPMap *tailcfg.DERPMap
+ SwaggerEndpoint bool
+ SetUserGroups func(ctx context.Context, tx database.Store, userID uuid.UUID, groupNames []string) error
+ TemplateScheduleStore provisionerdserver.TemplateScheduleStore
// APIRateLimit is the minutely throughput rate limit per user or ip.
// Setting a rate limit <0 will disable the rate limiter across the entire
@@ -206,6 +207,9 @@ func New(options *Options) *API {
if options.SetUserGroups == nil {
options.SetUserGroups = func(context.Context, database.Store, uuid.UUID, []string) error { return nil }
}
+ if options.TemplateScheduleStore == nil {
+ options.TemplateScheduleStore = provisionerdserver.NewAGPLTemplateScheduleStore()
+ }
siteCacheDir := options.CacheDir
if siteCacheDir != "" {
@@ -232,9 +236,10 @@ func New(options *Options) *API {
Authorizer: options.Authorizer,
Logger: options.Logger,
},
- metricsCache: metricsCache,
- Auditor: atomic.Pointer[audit.Auditor]{},
- Experiments: experiments,
+ metricsCache: metricsCache,
+ Auditor: atomic.Pointer[audit.Auditor]{},
+ TemplateScheduleStore: atomic.Pointer[provisionerdserver.TemplateScheduleStore]{},
+ Experiments: experiments,
}
if options.UpdateCheckOptions != nil {
api.updateChecker = updatecheck.New(
@@ -244,6 +249,7 @@ func New(options *Options) *API {
)
}
api.Auditor.Store(&options.Auditor)
+ api.TemplateScheduleStore.Store(&options.TemplateScheduleStore)
api.workspaceAgentCache = wsconncache.New(api.dialWorkspaceAgentTailnet, 0)
api.TailnetCoordinator.Store(&options.TailnetCoordinator)
oauthConfigs := &httpmw.OAuth2Configs{
@@ -679,6 +685,7 @@ type API struct {
WorkspaceClientCoordinateOverride atomic.Pointer[func(rw http.ResponseWriter) bool]
TailnetCoordinator atomic.Pointer[tailnet.Coordinator]
QuotaCommitter atomic.Pointer[proto.QuotaCommitter]
+ TemplateScheduleStore atomic.Pointer[provisionerdserver.TemplateScheduleStore]
HTTPAuth *HTTPAuthorizer
@@ -770,17 +777,18 @@ func (api *API) CreateInMemoryProvisionerDaemon(ctx context.Context, debounce ti
mux := drpcmux.New()
err = proto.DRPCRegisterProvisionerDaemon(mux, &provisionerdserver.Server{
- AccessURL: api.AccessURL,
- ID: daemon.ID,
- Database: api.Database,
- Pubsub: api.Pubsub,
- Provisioners: daemon.Provisioners,
- Telemetry: api.Telemetry,
- Tags: tags,
- QuotaCommitter: &api.QuotaCommitter,
- Auditor: &api.Auditor,
- AcquireJobDebounce: debounce,
- Logger: api.Logger.Named(fmt.Sprintf("provisionerd-%s", daemon.Name)),
+ AccessURL: api.AccessURL,
+ ID: daemon.ID,
+ Database: api.Database,
+ Pubsub: api.Pubsub,
+ Provisioners: daemon.Provisioners,
+ Telemetry: api.Telemetry,
+ Tags: tags,
+ QuotaCommitter: &api.QuotaCommitter,
+ Auditor: &api.Auditor,
+ TemplateScheduleStore: &api.TemplateScheduleStore,
+ AcquireJobDebounce: debounce,
+ Logger: api.Logger.Named(fmt.Sprintf("provisionerd-%s", daemon.Name)),
})
if err != nil {
return nil, err
diff --git a/coderd/database/models.go b/coderd/database/models.go
index d8826a06bac2b..d11366e9036e7 100644
--- a/coderd/database/models.go
+++ b/coderd/database/models.go
@@ -1418,7 +1418,7 @@ type Template struct {
DisplayName string `db:"display_name" json:"display_name"`
// Allow users to cancel in-progress workspace jobs.
AllowUserCancelWorkspaceJobs bool `db:"allow_user_cancel_workspace_jobs" json:"allow_user_cancel_workspace_jobs"`
- MaxTtl int64 `db:"max_ttl" json:"max_ttl"`
+ MaxTTL int64 `db:"max_ttl" json:"max_ttl"`
}
type TemplateVersion struct {
diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go
index 845fd933dd251..81ae8d2ca33ba 100644
--- a/coderd/database/queries.sql.go
+++ b/coderd/database/queries.sql.go
@@ -2401,7 +2401,7 @@ WHERE
-- Ensure the caller has the correct provisioner.
AND nested.provisioner = ANY($3 :: provisioner_type [ ])
-- Ensure the caller satisfies all job tags.
- AND nested.tags <@ $4 :: jsonb
+ AND nested.tags <@ $4 :: jsonb
ORDER BY
nested.created_at
FOR UPDATE
@@ -3117,7 +3117,7 @@ func (q *sqlQuerier) GetTemplateByID(ctx context.Context, id uuid.UUID) (Templat
&i.GroupACL,
&i.DisplayName,
&i.AllowUserCancelWorkspaceJobs,
- &i.MaxTtl,
+ &i.MaxTTL,
)
return i, err
}
@@ -3161,7 +3161,7 @@ func (q *sqlQuerier) GetTemplateByOrganizationAndName(ctx context.Context, arg G
&i.GroupACL,
&i.DisplayName,
&i.AllowUserCancelWorkspaceJobs,
- &i.MaxTtl,
+ &i.MaxTTL,
)
return i, err
}
@@ -3197,7 +3197,7 @@ func (q *sqlQuerier) GetTemplates(ctx context.Context) ([]Template, error) {
&i.GroupACL,
&i.DisplayName,
&i.AllowUserCancelWorkspaceJobs,
- &i.MaxTtl,
+ &i.MaxTTL,
); err != nil {
return nil, err
}
@@ -3281,7 +3281,7 @@ func (q *sqlQuerier) GetTemplatesWithFilter(ctx context.Context, arg GetTemplate
&i.GroupACL,
&i.DisplayName,
&i.AllowUserCancelWorkspaceJobs,
- &i.MaxTtl,
+ &i.MaxTTL,
); err != nil {
return nil, err
}
@@ -3373,7 +3373,7 @@ func (q *sqlQuerier) InsertTemplate(ctx context.Context, arg InsertTemplateParam
&i.GroupACL,
&i.DisplayName,
&i.AllowUserCancelWorkspaceJobs,
- &i.MaxTtl,
+ &i.MaxTTL,
)
return i, err
}
@@ -3416,7 +3416,7 @@ func (q *sqlQuerier) UpdateTemplateACLByID(ctx context.Context, arg UpdateTempla
&i.GroupACL,
&i.DisplayName,
&i.AllowUserCancelWorkspaceJobs,
- &i.MaxTtl,
+ &i.MaxTTL,
)
return i, err
}
@@ -3520,7 +3520,7 @@ func (q *sqlQuerier) UpdateTemplateMetaByID(ctx context.Context, arg UpdateTempl
&i.GroupACL,
&i.DisplayName,
&i.AllowUserCancelWorkspaceJobs,
- &i.MaxTtl,
+ &i.MaxTTL,
)
return i, err
}
diff --git a/coderd/database/sqlc.yaml b/coderd/database/sqlc.yaml
index e53fead06d0de..26f40fbd4d5e9 100644
--- a/coderd/database/sqlc.yaml
+++ b/coderd/database/sqlc.yaml
@@ -42,6 +42,7 @@ overrides:
group_acl: GroupACL
troubleshooting_url: TroubleshootingURL
default_ttl: DefaultTTL
+ max_ttl: MaxTTL
motd_file: MOTDFile
sql:
diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go
index 96d90c2dd85ac..1fb3b4f277e02 100644
--- a/coderd/provisionerdserver/provisionerdserver.go
+++ b/coderd/provisionerdserver/provisionerdserver.go
@@ -40,16 +40,17 @@ var (
)
type Server struct {
- AccessURL *url.URL
- ID uuid.UUID
- Logger slog.Logger
- Provisioners []database.ProvisionerType
- Tags json.RawMessage
- Database database.Store
- Pubsub database.Pubsub
- Telemetry telemetry.Reporter
- QuotaCommitter *atomic.Pointer[proto.QuotaCommitter]
- Auditor *atomic.Pointer[audit.Auditor]
+ AccessURL *url.URL
+ ID uuid.UUID
+ Logger slog.Logger
+ Provisioners []database.ProvisionerType
+ Tags json.RawMessage
+ Database database.Store
+ Pubsub database.Pubsub
+ Telemetry telemetry.Reporter
+ QuotaCommitter *atomic.Pointer[proto.QuotaCommitter]
+ Auditor *atomic.Pointer[audit.Auditor]
+ TemplateScheduleStore *atomic.Pointer[TemplateScheduleStore]
AcquireJobDebounce time.Duration
}
@@ -520,12 +521,12 @@ func (server *Server) FailJob(ctx context.Context, failJob *proto.FailedJob) (*p
var build database.WorkspaceBuild
err := server.Database.InTx(func(db database.Store) error {
- workspaceBuild, err := server.Database.GetWorkspaceBuildByID(ctx, input.WorkspaceBuildID)
+ workspaceBuild, err := db.GetWorkspaceBuildByID(ctx, input.WorkspaceBuildID)
if err != nil {
return xerrors.Errorf("get workspace build: %w", err)
}
- build, err = server.Database.UpdateWorkspaceBuildByID(ctx, database.UpdateWorkspaceBuildByIDParams{
+ build, err = db.UpdateWorkspaceBuildByID(ctx, database.UpdateWorkspaceBuildByIDParams{
ID: input.WorkspaceBuildID,
UpdatedAt: database.Now(),
ProvisionerState: jobType.WorkspaceBuild.State,
@@ -611,6 +612,8 @@ func (server *Server) FailJob(ctx context.Context, failJob *proto.FailedJob) (*p
}
// CompleteJob is triggered by a provision daemon to mark a provisioner job as completed.
+//
+//nolint:gocyclo
func (server *Server) CompleteJob(ctx context.Context, completed *proto.CompletedJob) (*proto.Empty, error) {
jobID, err := uuid.Parse(completed.JobId)
if err != nil {
@@ -715,39 +718,49 @@ func (server *Server) CompleteJob(ctx context.Context, completed *proto.Complete
var getWorkspaceError error
err = server.Database.InTx(func(db database.Store) error {
- now := database.Now()
- var workspaceDeadline time.Time
+ var (
+ now = database.Now()
+ // deadline is the time when the workspace will be stopped. The
+ // value can be bumped by user activity or manually by the user
+ // via the UI.
+ deadline time.Time
+ // maxDeadline is the maximum value for deadline.
+ maxDeadline time.Time
+ )
+
workspace, getWorkspaceError = db.GetWorkspaceByID(ctx, workspaceBuild.WorkspaceID)
- if getWorkspaceError == nil {
- if workspace.Ttl.Valid {
- workspaceDeadline = now.Add(time.Duration(workspace.Ttl.Int64))
- }
- } else {
- // Huh? Did the workspace get deleted?
- // In any case, since this is just for the TTL, try and continue anyway.
+ if getWorkspaceError != nil {
server.Logger.Error(ctx,
"fetch workspace for build",
slog.F("workspace_build_id", workspaceBuild.ID),
slog.F("workspace_id", workspaceBuild.WorkspaceID),
)
+ return getWorkspaceError
+ }
+ if workspace.Ttl.Valid {
+ deadline = now.Add(time.Duration(workspace.Ttl.Int64))
}
- var workspaceMaxDeadline time.Time
- template, err := db.GetTemplateByID(ctx, workspace.TemplateID)
- if err == nil {
- if template.MaxTtl > 0 {
- workspaceMaxDeadline = now.Add(time.Duration(template.MaxTtl))
- if !workspaceDeadline.IsZero() && workspaceMaxDeadline.Before(workspaceDeadline) {
- workspaceDeadline = workspaceMaxDeadline
- }
+ templateSchedule, err := (*server.TemplateScheduleStore.Load()).GetTemplateScheduleOptions(ctx, db, workspace.TemplateID)
+ if err != nil {
+ return xerrors.Errorf("get template schedule options: %w", err)
+ }
+ if !templateSchedule.UserSchedulingEnabled {
+ // The user is not permitted to set their own TTL.
+ deadline = time.Time{}
+ }
+ if deadline.IsZero() && templateSchedule.DefaultTTL > 0 {
+ deadline = now.Add(templateSchedule.DefaultTTL)
+ }
+ if templateSchedule.MaxTTL > 0 {
+ maxDeadline = now.Add(templateSchedule.MaxTTL)
+
+ if deadline.IsZero() || maxDeadline.Before(deadline) {
+ // If the workspace doesn't have a deadline or the max
+ // deadline is sooner than the workspace deadline, use the
+ // max deadline as the actual deadline.
+ deadline = maxDeadline
}
- } else {
- server.Logger.Error(ctx,
- "fetch template for build",
- slog.F("workspace_build_id", workspaceBuild.ID),
- slog.F("workspace_id", workspaceBuild.WorkspaceID),
- slog.F("template_id", workspace.TemplateID),
- )
}
err = db.UpdateProvisionerJobWithCompleteByID(ctx, database.UpdateProvisionerJobWithCompleteByIDParams{
@@ -763,8 +776,8 @@ func (server *Server) CompleteJob(ctx context.Context, completed *proto.Complete
}
_, err = db.UpdateWorkspaceBuildByID(ctx, database.UpdateWorkspaceBuildByIDParams{
ID: workspaceBuild.ID,
- Deadline: workspaceDeadline,
- MaxDeadline: workspaceMaxDeadline,
+ Deadline: deadline,
+ MaxDeadline: maxDeadline,
ProvisionerState: jobType.WorkspaceBuild.State,
UpdatedAt: now,
})
diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go
index 613e1fef2d8ba..49a54dcdcfa3b 100644
--- a/coderd/provisionerdserver/provisionerdserver_test.go
+++ b/coderd/provisionerdserver/provisionerdserver_test.go
@@ -31,6 +31,13 @@ func mockAuditor() *atomic.Pointer[audit.Auditor] {
return ptr
}
+func testTemplateScheduleStore() *atomic.Pointer[provisionerdserver.TemplateScheduleStore] {
+ ptr := &atomic.Pointer[provisionerdserver.TemplateScheduleStore]{}
+ store := provisionerdserver.NewAGPLTemplateScheduleStore()
+ ptr.Store(&store)
+ return ptr
+}
+
func TestAcquireJob(t *testing.T) {
t.Parallel()
t.Run("Debounce", func(t *testing.T) {
@@ -38,15 +45,16 @@ func TestAcquireJob(t *testing.T) {
db := dbfake.New()
pubsub := database.NewPubsubInMemory()
srv := &provisionerdserver.Server{
- ID: uuid.New(),
- Logger: slogtest.Make(t, nil),
- AccessURL: &url.URL{},
- Provisioners: []database.ProvisionerType{database.ProvisionerTypeEcho},
- Database: db,
- Pubsub: pubsub,
- Telemetry: telemetry.NewNoop(),
- AcquireJobDebounce: time.Hour,
- Auditor: mockAuditor(),
+ ID: uuid.New(),
+ Logger: slogtest.Make(t, nil),
+ AccessURL: &url.URL{},
+ Provisioners: []database.ProvisionerType{database.ProvisionerTypeEcho},
+ Database: db,
+ Pubsub: pubsub,
+ Telemetry: telemetry.NewNoop(),
+ AcquireJobDebounce: time.Hour,
+ Auditor: mockAuditor(),
+ TemplateScheduleStore: testTemplateScheduleStore(),
}
job, err := srv.AcquireJob(context.Background(), nil)
require.NoError(t, err)
@@ -602,27 +610,42 @@ func TestCompleteJob(t *testing.T) {
t.Run("WorkspaceBuild", func(t *testing.T) {
t.Parallel()
srv := setup(t, false)
+
+ user := dbgen.User(t, srv.Database, database.User{})
+ template := dbgen.Template(t, srv.Database, database.Template{
+ Name: "template",
+ Provisioner: database.ProvisionerTypeEcho,
+ })
+ file := dbgen.File(t, srv.Database, database.File{CreatedBy: user.ID})
workspace, err := srv.Database.InsertWorkspace(ctx, database.InsertWorkspaceParams{
- ID: uuid.New(),
+ ID: uuid.New(),
+ TemplateID: template.ID,
})
- require.NoError(t, err)
- build, err := srv.Database.InsertWorkspaceBuild(ctx, database.InsertWorkspaceBuildParams{
- ID: uuid.New(),
- WorkspaceID: workspace.ID,
- Transition: database.WorkspaceTransitionDelete,
- Reason: database.BuildReasonInitiator,
+ version := dbgen.TemplateVersion(t, srv.Database, database.TemplateVersion{
+ TemplateID: uuid.NullUUID{
+ UUID: template.ID,
+ Valid: true,
+ },
+ JobID: uuid.New(),
})
require.NoError(t, err)
- input, err := json.Marshal(provisionerdserver.WorkspaceProvisionJob{
- WorkspaceBuildID: build.ID,
+ build, err := srv.Database.InsertWorkspaceBuild(ctx, database.InsertWorkspaceBuildParams{
+ ID: uuid.New(),
+ WorkspaceID: workspace.ID,
+ TemplateVersionID: version.ID,
+ Transition: database.WorkspaceTransitionDelete,
+ Reason: database.BuildReasonInitiator,
})
require.NoError(t, err)
job, err := srv.Database.InsertProvisionerJob(ctx, database.InsertProvisionerJobParams{
ID: uuid.New(),
+ FileID: file.ID,
Provisioner: database.ProvisionerTypeEcho,
- Input: input,
Type: database.ProvisionerJobTypeWorkspaceBuild,
StorageMethod: database.ProvisionerStorageMethodFile,
+ Input: must(json.Marshal(provisionerdserver.WorkspaceProvisionJob{
+ WorkspaceBuildID: build.ID,
+ })),
})
require.NoError(t, err)
_, err = srv.Database.AcquireProvisionerJob(ctx, database.AcquireProvisionerJobParams{
@@ -802,14 +825,15 @@ func setup(t *testing.T, ignoreLogErrors bool) *provisionerdserver.Server {
pubsub := database.NewPubsubInMemory()
return &provisionerdserver.Server{
- ID: uuid.New(),
- Logger: slogtest.Make(t, &slogtest.Options{IgnoreErrors: ignoreLogErrors}),
- AccessURL: &url.URL{},
- Provisioners: []database.ProvisionerType{database.ProvisionerTypeEcho},
- Database: db,
- Pubsub: pubsub,
- Telemetry: telemetry.NewNoop(),
- Auditor: mockAuditor(),
+ ID: uuid.New(),
+ Logger: slogtest.Make(t, &slogtest.Options{IgnoreErrors: ignoreLogErrors}),
+ AccessURL: &url.URL{},
+ Provisioners: []database.ProvisionerType{database.ProvisionerTypeEcho},
+ Database: db,
+ Pubsub: pubsub,
+ Telemetry: telemetry.NewNoop(),
+ Auditor: mockAuditor(),
+ TemplateScheduleStore: testTemplateScheduleStore(),
}
}
diff --git a/coderd/provisionerdserver/templatescheduleoptions.go b/coderd/provisionerdserver/templatescheduleoptions.go
new file mode 100644
index 0000000000000..c6eaf45c3a018
--- /dev/null
+++ b/coderd/provisionerdserver/templatescheduleoptions.go
@@ -0,0 +1,47 @@
+package provisionerdserver
+
+import (
+ "context"
+ "time"
+
+ "github.com/google/uuid"
+
+ "github.com/coder/coder/coderd/database"
+)
+
+type TemplateScheduleOptions struct {
+ UserSchedulingEnabled bool `json:"user_scheduling_enabled"`
+ DefaultTTL time.Duration `json:"default_ttl"`
+ // If MaxTTL is set, the workspace must be stopped before this time or it
+ // will be stopped automatically.
+ //
+ // If set, users cannot disable automatic workspace shutdown.
+ MaxTTL time.Duration `json:"max_ttl"`
+}
+
+// TemplateScheduleStore provides an interface for retrieving template
+// scheduling options set by the template/site admin.
+type TemplateScheduleStore interface {
+ GetTemplateScheduleOptions(ctx context.Context, db database.Store, templateID uuid.UUID) (TemplateScheduleOptions, error)
+}
+
+type agplTemplateScheduleStore struct{}
+
+var _ TemplateScheduleStore = &agplTemplateScheduleStore{}
+
+func NewAGPLTemplateScheduleStore() TemplateScheduleStore {
+ return &agplTemplateScheduleStore{}
+}
+
+func (s *agplTemplateScheduleStore) GetTemplateScheduleOptions(ctx context.Context, db database.Store, templateID uuid.UUID) (TemplateScheduleOptions, error) {
+ tpl, err := db.GetTemplateByID(ctx, templateID)
+ if err != nil {
+ return TemplateScheduleOptions{}, err
+ }
+
+ return TemplateScheduleOptions{
+ UserSchedulingEnabled: true,
+ DefaultTTL: time.Duration(tpl.DefaultTTL),
+ MaxTTL: 0,
+ }, nil
+}
diff --git a/coderd/templates.go b/coderd/templates.go
index febe8ab84c93b..b6d1fdd22ffc5 100644
--- a/coderd/templates.go
+++ b/coderd/templates.go
@@ -475,7 +475,7 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
displayName := req.DisplayName
desc := req.Description
icon := req.Icon
- maxTTL := time.Duration(req.DefaultTTLMillis) * time.Millisecond
+ defaultTTL := time.Duration(req.DefaultTTLMillis) * time.Millisecond
allowUserCancelWorkspaceJobs := req.AllowUserCancelWorkspaceJobs
if name == "" {
@@ -493,7 +493,7 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
DisplayName: displayName,
Description: desc,
Icon: icon,
- DefaultTTL: int64(maxTTL),
+ DefaultTTL: int64(defaultTTL),
AllowUserCancelWorkspaceJobs: allowUserCancelWorkspaceJobs,
})
if err != nil {
diff --git a/coderd/templates_test.go b/coderd/templates_test.go
index 482bef10cda72..6cb68e5cde3e8 100644
--- a/coderd/templates_test.go
+++ b/coderd/templates_test.go
@@ -87,7 +87,7 @@ func TestPostTemplateByOrganization(t *testing.T) {
require.Equal(t, http.StatusConflict, apiErr.StatusCode())
})
- t.Run("MaxTTLTooLow", func(t *testing.T) {
+ t.Run("DefaultTTLTooLow", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
@@ -107,7 +107,7 @@ func TestPostTemplateByOrganization(t *testing.T) {
require.Contains(t, err.Error(), "default_ttl_ms: Must be a positive integer")
})
- t.Run("NoMaxTTL", func(t *testing.T) {
+ t.Run("NoDefaultTTL", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
@@ -290,7 +290,7 @@ func TestPatchTemplateMeta(t *testing.T) {
assert.Equal(t, database.AuditActionWrite, auditor.AuditLogs[4].Action)
})
- t.Run("NoMaxTTL", func(t *testing.T) {
+ t.Run("NoDefaultTTL", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
@@ -319,7 +319,7 @@ func TestPatchTemplateMeta(t *testing.T) {
assert.Equal(t, req.DefaultTTLMillis, updated.DefaultTTLMillis)
})
- t.Run("MaxTTLTooLow", func(t *testing.T) {
+ t.Run("DefaultTTLTooLow", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
diff --git a/codersdk/deployment.go b/codersdk/deployment.go
index da4a68e6753a8..e01f17ed77f6c 100644
--- a/codersdk/deployment.go
+++ b/codersdk/deployment.go
@@ -35,6 +35,7 @@ const (
FeatureMultipleGitAuth FeatureName = "multiple_git_auth"
FeatureExternalProvisionerDaemons FeatureName = "external_provisioner_daemons"
FeatureAppearance FeatureName = "appearance"
+ FeatureAdvancedTemplateScheduling FeatureName = "advanced_template_scheduling"
)
// FeatureNames must be kept in-sync with the Feature enum above.
@@ -48,6 +49,7 @@ var FeatureNames = []FeatureName{
FeatureMultipleGitAuth,
FeatureExternalProvisionerDaemons,
FeatureAppearance,
+ FeatureAdvancedTemplateScheduling,
}
// Humanize returns the feature name in a human-readable format.
diff --git a/enterprise/audit/table.go b/enterprise/audit/table.go
index ffc0f303bd25a..256af036cb00c 100644
--- a/enterprise/audit/table.go
+++ b/enterprise/audit/table.go
@@ -71,6 +71,7 @@ var AuditableResources = auditMap(map[any]map[string]Action{
"group_acl": ActionTrack,
"user_acl": ActionTrack,
"allow_user_cancel_workspace_jobs": ActionTrack,
+ "max_ttl": ActionTrack,
},
&database.TemplateVersion{}: {
"id": ActionTrack,
diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go
index 67d5b543aef31..03fefd59e8596 100644
--- a/enterprise/coderd/coderd.go
+++ b/enterprise/coderd/coderd.go
@@ -20,6 +20,7 @@ import (
agplaudit "github.com/coder/coder/coderd/audit"
"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/coderd/httpmw"
+ "github.com/coder/coder/coderd/provisionerdserver"
"github.com/coder/coder/coderd/rbac"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/enterprise/coderd/license"
@@ -248,6 +249,7 @@ func (api *API) updateEntitlements(ctx context.Context) error {
codersdk.FeatureMultipleGitAuth: len(api.GitAuthConfigs) > 1,
codersdk.FeatureTemplateRBAC: api.RBAC,
codersdk.FeatureExternalProvisionerDaemons: true,
+ codersdk.FeatureAdvancedTemplateScheduling: true,
})
if err != nil {
return err
@@ -292,6 +294,17 @@ func (api *API) updateEntitlements(ctx context.Context) error {
}
}
+ if changed, enabled := featureChanged(codersdk.FeatureAdvancedTemplateScheduling); changed {
+ if enabled {
+ store := &enterpriseTemplateScheduleStore{}
+ ptr := provisionerdserver.TemplateScheduleStore(store)
+ api.AGPL.TemplateScheduleStore.Store(&ptr)
+ } else {
+ store := provisionerdserver.NewAGPLTemplateScheduleStore()
+ api.AGPL.TemplateScheduleStore.Store(&store)
+ }
+ }
+
if changed, enabled := featureChanged(codersdk.FeatureHighAvailability); changed {
coordinator := agpltailnet.NewCoordinator()
if enabled {
diff --git a/enterprise/coderd/coderd_test.go b/enterprise/coderd/coderd_test.go
index 4d67d97029830..a15b1ba71f7f1 100644
--- a/enterprise/coderd/coderd_test.go
+++ b/enterprise/coderd/coderd_test.go
@@ -47,6 +47,7 @@ func TestEntitlements(t *testing.T) {
codersdk.FeatureAuditLog: 1,
codersdk.FeatureTemplateRBAC: 1,
codersdk.FeatureExternalProvisionerDaemons: 1,
+ codersdk.FeatureAdvancedTemplateScheduling: 1,
},
})
res, err := client.Entitlements(context.Background())
diff --git a/enterprise/coderd/provisionerdaemons.go b/enterprise/coderd/provisionerdaemons.go
index 7fbc5b42b17c1..59512eaa385ff 100644
--- a/enterprise/coderd/provisionerdaemons.go
+++ b/enterprise/coderd/provisionerdaemons.go
@@ -1,6 +1,7 @@
package coderd
import (
+ "context"
"database/sql"
"encoding/json"
"errors"
@@ -8,6 +9,7 @@ import (
"io"
"net/http"
"strings"
+ "time"
"github.com/google/uuid"
"github.com/hashicorp/yamux"
@@ -210,15 +212,16 @@ func (api *API) provisionerDaemonServe(rw http.ResponseWriter, r *http.Request)
}
mux := drpcmux.New()
err = proto.DRPCRegisterProvisionerDaemon(mux, &provisionerdserver.Server{
- AccessURL: api.AccessURL,
- ID: daemon.ID,
- Database: api.Database,
- Pubsub: api.Pubsub,
- Provisioners: daemon.Provisioners,
- Telemetry: api.Telemetry,
- Auditor: &api.AGPL.Auditor,
- Logger: api.Logger.Named(fmt.Sprintf("provisionerd-%s", daemon.Name)),
- Tags: rawTags,
+ AccessURL: api.AccessURL,
+ ID: daemon.ID,
+ Database: api.Database,
+ Pubsub: api.Pubsub,
+ Provisioners: daemon.Provisioners,
+ Telemetry: api.Telemetry,
+ Auditor: &api.AGPL.Auditor,
+ TemplateScheduleStore: &api.AGPL.TemplateScheduleStore,
+ Logger: api.Logger.Named(fmt.Sprintf("provisionerd-%s", daemon.Name)),
+ Tags: rawTags,
})
if err != nil {
_ = conn.Close(websocket.StatusInternalError, httpapi.WebsocketCloseSprintf("drpc register provisioner daemon: %s", err))
@@ -254,3 +257,21 @@ func convertProvisionerDaemon(daemon database.ProvisionerDaemon) codersdk.Provis
}
return result
}
+
+type enterpriseTemplateScheduleStore struct{}
+
+var _ provisionerdserver.TemplateScheduleStore = &enterpriseTemplateScheduleStore{}
+
+func (s *enterpriseTemplateScheduleStore) GetTemplateScheduleOptions(ctx context.Context, db database.Store, templateID uuid.UUID) (provisionerdserver.TemplateScheduleOptions, error) {
+ tpl, err := db.GetTemplateByID(ctx, templateID)
+ if err != nil {
+ return provisionerdserver.TemplateScheduleOptions{}, err
+ }
+
+ return provisionerdserver.TemplateScheduleOptions{
+ // TODO: make configurable at template level
+ UserSchedulingEnabled: true,
+ DefaultTTL: time.Duration(tpl.DefaultTTL),
+ MaxTTL: time.Duration(tpl.MaxTTL),
+ }, nil
+}
From 9e37cc73cf7238e73002b17f7c142af075e61937 Mon Sep 17 00:00:00 2001
From: Dean Sheather
Date: Mon, 20 Feb 2023 14:04:07 +0000
Subject: [PATCH 03/24] feat: add API for setting template max_ttl
---
coderd/apidoc/docs.go | 4 ++
coderd/apidoc/swagger.json | 4 ++
coderd/database/dbauthz/querier.go | 7 +++
coderd/database/dbfake/databasefake.go | 25 +++++++-
coderd/database/querier.go | 1 +
coderd/database/queries.sql.go | 61 ++++++++++++++++---
coderd/database/queries/provisionerjobs.sql | 2 +-
coderd/database/queries/templates.sql | 21 +++++--
.../templatescheduleoptions.go | 18 +++++-
coderd/templates.go | 18 +++++-
coderd/templates_test.go | 24 ++++++++
codersdk/templates.go | 23 ++++---
docs/admin/audit-logs.md | 22 +++----
docs/api/schemas.md | 38 ++++++------
docs/api/templates.md | 50 ++++++++-------
enterprise/coderd/provisionerdaemons.go | 11 +++-
site/src/api/typesGenerated.ts | 4 ++
17 files changed, 253 insertions(+), 80 deletions(-)
diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go
index d4e1236849534..b2f224135c8b2 100644
--- a/coderd/apidoc/docs.go
+++ b/coderd/apidoc/docs.go
@@ -7463,6 +7463,10 @@ const docTemplate = `{
"type": "string",
"format": "uuid"
},
+ "max_ttl_ms": {
+ "description": "MaxTTLMillis is an enterprise feature. It's value is only used if your\nlicense is entitled to use the advanced template scheduling feature.",
+ "type": "integer"
+ },
"name": {
"type": "string"
},
diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json
index 894a2d0b6cafd..c777743e6d011 100644
--- a/coderd/apidoc/swagger.json
+++ b/coderd/apidoc/swagger.json
@@ -6709,6 +6709,10 @@
"type": "string",
"format": "uuid"
},
+ "max_ttl_ms": {
+ "description": "MaxTTLMillis is an enterprise feature. It's value is only used if your\nlicense is entitled to use the advanced template scheduling feature.",
+ "type": "integer"
+ },
"name": {
"type": "string"
},
diff --git a/coderd/database/dbauthz/querier.go b/coderd/database/dbauthz/querier.go
index 91e912cb90271..51d87e12365f6 100644
--- a/coderd/database/dbauthz/querier.go
+++ b/coderd/database/dbauthz/querier.go
@@ -852,6 +852,13 @@ func (q *querier) UpdateTemplateMetaByID(ctx context.Context, arg database.Updat
return updateWithReturn(q.log, q.auth, fetch, q.db.UpdateTemplateMetaByID)(ctx, arg)
}
+func (q *querier) UpdateTemplateScheduleByID(ctx context.Context, arg database.UpdateTemplateScheduleByIDParams) (database.Template, error) {
+ fetch := func(ctx context.Context, arg database.UpdateTemplateScheduleByIDParams) (database.Template, error) {
+ return q.db.GetTemplateByID(ctx, arg.ID)
+ }
+ return updateWithReturn(q.log, q.auth, fetch, q.db.UpdateTemplateScheduleByID)(ctx, arg)
+}
+
func (q *querier) UpdateTemplateVersionByID(ctx context.Context, arg database.UpdateTemplateVersionByIDParams) error {
template, err := q.db.GetTemplateByID(ctx, arg.TemplateID.UUID)
if err != nil {
diff --git a/coderd/database/dbfake/databasefake.go b/coderd/database/dbfake/databasefake.go
index 1426eb494833f..bce496e205582 100644
--- a/coderd/database/dbfake/databasefake.go
+++ b/coderd/database/dbfake/databasefake.go
@@ -1644,8 +1644,8 @@ func (q *fakeQuerier) UpdateTemplateMetaByID(_ context.Context, arg database.Upd
return database.Template{}, err
}
- q.mutex.RLock()
- defer q.mutex.RUnlock()
+ q.mutex.Lock()
+ defer q.mutex.Unlock()
for idx, tpl := range q.templates {
if tpl.ID != arg.ID {
@@ -1656,7 +1656,28 @@ func (q *fakeQuerier) UpdateTemplateMetaByID(_ context.Context, arg database.Upd
tpl.DisplayName = arg.DisplayName
tpl.Description = arg.Description
tpl.Icon = arg.Icon
+ q.templates[idx] = tpl
+ return tpl, nil
+ }
+
+ return database.Template{}, sql.ErrNoRows
+}
+
+func (q *fakeQuerier) UpdateTemplateScheduleByID(_ context.Context, arg database.UpdateTemplateScheduleByIDParams) (database.Template, error) {
+ if err := validateDatabaseType(arg); err != nil {
+ return database.Template{}, err
+ }
+
+ q.mutex.Lock()
+ defer q.mutex.Unlock()
+
+ for idx, tpl := range q.templates {
+ if tpl.ID != arg.ID {
+ continue
+ }
+ tpl.UpdatedAt = database.Now()
tpl.DefaultTTL = arg.DefaultTTL
+ tpl.MaxTTL = arg.MaxTTL
q.templates[idx] = tpl
return tpl, nil
}
diff --git a/coderd/database/querier.go b/coderd/database/querier.go
index 4f7bf73b4de3e..bdf82c85d1d2d 100644
--- a/coderd/database/querier.go
+++ b/coderd/database/querier.go
@@ -187,6 +187,7 @@ type sqlcQuerier interface {
UpdateTemplateActiveVersionByID(ctx context.Context, arg UpdateTemplateActiveVersionByIDParams) error
UpdateTemplateDeletedByID(ctx context.Context, arg UpdateTemplateDeletedByIDParams) error
UpdateTemplateMetaByID(ctx context.Context, arg UpdateTemplateMetaByIDParams) (Template, error)
+ UpdateTemplateScheduleByID(ctx context.Context, arg UpdateTemplateScheduleByIDParams) (Template, error)
UpdateTemplateVersionByID(ctx context.Context, arg UpdateTemplateVersionByIDParams) error
UpdateTemplateVersionDescriptionByJobID(ctx context.Context, arg UpdateTemplateVersionDescriptionByJobIDParams) error
UpdateUserDeletedByID(ctx context.Context, arg UpdateUserDeletedByIDParams) error
diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go
index a31e50bff9070..2278594709f16 100644
--- a/coderd/database/queries.sql.go
+++ b/coderd/database/queries.sql.go
@@ -3493,11 +3493,10 @@ UPDATE
SET
updated_at = $2,
description = $3,
- default_ttl = $4,
- name = $5,
- icon = $6,
- display_name = $7,
- allow_user_cancel_workspace_jobs = $8
+ name = $4,
+ icon = $5,
+ display_name = $6,
+ allow_user_cancel_workspace_jobs = $7
WHERE
id = $1
RETURNING
@@ -3508,7 +3507,6 @@ type UpdateTemplateMetaByIDParams struct {
ID uuid.UUID `db:"id" json:"id"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
Description string `db:"description" json:"description"`
- DefaultTTL int64 `db:"default_ttl" json:"default_ttl"`
Name string `db:"name" json:"name"`
Icon string `db:"icon" json:"icon"`
DisplayName string `db:"display_name" json:"display_name"`
@@ -3520,7 +3518,6 @@ func (q *sqlQuerier) UpdateTemplateMetaByID(ctx context.Context, arg UpdateTempl
arg.ID,
arg.UpdatedAt,
arg.Description,
- arg.DefaultTTL,
arg.Name,
arg.Icon,
arg.DisplayName,
@@ -3549,6 +3546,56 @@ func (q *sqlQuerier) UpdateTemplateMetaByID(ctx context.Context, arg UpdateTempl
return i, err
}
+const updateTemplateScheduleByID = `-- name: UpdateTemplateScheduleByID :one
+UPDATE
+ templates
+SET
+ updated_at = $2,
+ default_ttl = $3,
+ max_ttl = $4
+WHERE
+ id = $1
+RETURNING
+ id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl
+`
+
+type UpdateTemplateScheduleByIDParams struct {
+ ID uuid.UUID `db:"id" json:"id"`
+ UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
+ DefaultTTL int64 `db:"default_ttl" json:"default_ttl"`
+ MaxTTL int64 `db:"max_ttl" json:"max_ttl"`
+}
+
+func (q *sqlQuerier) UpdateTemplateScheduleByID(ctx context.Context, arg UpdateTemplateScheduleByIDParams) (Template, error) {
+ row := q.db.QueryRowContext(ctx, updateTemplateScheduleByID,
+ arg.ID,
+ arg.UpdatedAt,
+ arg.DefaultTTL,
+ arg.MaxTTL,
+ )
+ var i Template
+ err := row.Scan(
+ &i.ID,
+ &i.CreatedAt,
+ &i.UpdatedAt,
+ &i.OrganizationID,
+ &i.Deleted,
+ &i.Name,
+ &i.Provisioner,
+ &i.ActiveVersionID,
+ &i.Description,
+ &i.DefaultTTL,
+ &i.CreatedBy,
+ &i.Icon,
+ &i.UserACL,
+ &i.GroupACL,
+ &i.DisplayName,
+ &i.AllowUserCancelWorkspaceJobs,
+ &i.MaxTTL,
+ )
+ return i, err
+}
+
const getTemplateVersionParameters = `-- name: GetTemplateVersionParameters :many
SELECT template_version_id, name, description, type, mutable, default_value, icon, options, validation_regex, validation_min, validation_max, validation_error, validation_monotonic FROM template_version_parameters WHERE template_version_id = $1
`
diff --git a/coderd/database/queries/provisionerjobs.sql b/coderd/database/queries/provisionerjobs.sql
index 8f68546a58ef3..7626c52251280 100644
--- a/coderd/database/queries/provisionerjobs.sql
+++ b/coderd/database/queries/provisionerjobs.sql
@@ -22,7 +22,7 @@ WHERE
-- Ensure the caller has the correct provisioner.
AND nested.provisioner = ANY(@types :: provisioner_type [ ])
-- Ensure the caller satisfies all job tags.
- AND nested.tags <@ @tags :: jsonb
+ AND nested.tags <@ @tags :: jsonb
ORDER BY
nested.created_at
FOR UPDATE
diff --git a/coderd/database/queries/templates.sql b/coderd/database/queries/templates.sql
index 5fa8270b5ae33..30cccd40b033d 100644
--- a/coderd/database/queries/templates.sql
+++ b/coderd/database/queries/templates.sql
@@ -102,11 +102,22 @@ UPDATE
SET
updated_at = $2,
description = $3,
- default_ttl = $4,
- name = $5,
- icon = $6,
- display_name = $7,
- allow_user_cancel_workspace_jobs = $8
+ name = $4,
+ icon = $5,
+ display_name = $6,
+ allow_user_cancel_workspace_jobs = $7
+WHERE
+ id = $1
+RETURNING
+ *;
+
+-- name: UpdateTemplateScheduleByID :one
+UPDATE
+ templates
+SET
+ updated_at = $2,
+ default_ttl = $3,
+ max_ttl = $4
WHERE
id = $1
RETURNING
diff --git a/coderd/provisionerdserver/templatescheduleoptions.go b/coderd/provisionerdserver/templatescheduleoptions.go
index c6eaf45c3a018..2387115c4f0b5 100644
--- a/coderd/provisionerdserver/templatescheduleoptions.go
+++ b/coderd/provisionerdserver/templatescheduleoptions.go
@@ -23,6 +23,7 @@ type TemplateScheduleOptions struct {
// scheduling options set by the template/site admin.
type TemplateScheduleStore interface {
GetTemplateScheduleOptions(ctx context.Context, db database.Store, templateID uuid.UUID) (TemplateScheduleOptions, error)
+ SetTemplateScheduleOptions(ctx context.Context, db database.Store, template database.Template, opts TemplateScheduleOptions) (database.Template, error)
}
type agplTemplateScheduleStore struct{}
@@ -33,7 +34,7 @@ func NewAGPLTemplateScheduleStore() TemplateScheduleStore {
return &agplTemplateScheduleStore{}
}
-func (s *agplTemplateScheduleStore) GetTemplateScheduleOptions(ctx context.Context, db database.Store, templateID uuid.UUID) (TemplateScheduleOptions, error) {
+func (*agplTemplateScheduleStore) GetTemplateScheduleOptions(ctx context.Context, db database.Store, templateID uuid.UUID) (TemplateScheduleOptions, error) {
tpl, err := db.GetTemplateByID(ctx, templateID)
if err != nil {
return TemplateScheduleOptions{}, err
@@ -42,6 +43,19 @@ func (s *agplTemplateScheduleStore) GetTemplateScheduleOptions(ctx context.Conte
return TemplateScheduleOptions{
UserSchedulingEnabled: true,
DefaultTTL: time.Duration(tpl.DefaultTTL),
- MaxTTL: 0,
+ // Disregard the value in the database, since MaxTTL is an enterprise
+ // feature.
+ MaxTTL: 0,
}, nil
}
+
+func (*agplTemplateScheduleStore) SetTemplateScheduleOptions(ctx context.Context, db database.Store, tpl database.Template, opts TemplateScheduleOptions) (database.Template, error) {
+ return db.UpdateTemplateScheduleByID(ctx, database.UpdateTemplateScheduleByIDParams{
+ ID: tpl.ID,
+ UpdatedAt: database.Now(),
+ DefaultTTL: int64(opts.DefaultTTL),
+ // Don't allow changing it, but keep the value in the DB (to avoid
+ // clearing settings if the license has an issue).
+ MaxTTL: tpl.MaxTTL,
+ })
+}
diff --git a/coderd/templates.go b/coderd/templates.go
index 3e9bc345e24f1..21d47b138ac16 100644
--- a/coderd/templates.go
+++ b/coderd/templates.go
@@ -17,6 +17,7 @@ import (
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/coderd/httpmw"
+ "github.com/coder/coder/coderd/provisionerdserver"
"github.com/coder/coder/coderd/rbac"
"github.com/coder/coder/coderd/telemetry"
"github.com/coder/coder/codersdk"
@@ -479,7 +480,6 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
displayName := req.DisplayName
desc := req.Description
icon := req.Icon
- defaultTTL := time.Duration(req.DefaultTTLMillis) * time.Millisecond
allowUserCancelWorkspaceJobs := req.AllowUserCancelWorkspaceJobs
if name == "" {
@@ -497,11 +497,23 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
DisplayName: displayName,
Description: desc,
Icon: icon,
- DefaultTTL: int64(defaultTTL),
AllowUserCancelWorkspaceJobs: allowUserCancelWorkspaceJobs,
})
if err != nil {
- return err
+ return xerrors.Errorf("update template metadata: %w", err)
+ }
+
+ defaultTTL := time.Duration(req.DefaultTTLMillis) * time.Millisecond
+ maxTTL := time.Duration(req.MaxTTLMillis) * time.Millisecond
+ if defaultTTL != time.Duration(template.DefaultTTL) || maxTTL != time.Duration(template.MaxTTL) {
+ updated, err = (*api.TemplateScheduleStore.Load()).SetTemplateScheduleOptions(ctx, tx, updated, provisionerdserver.TemplateScheduleOptions{
+ UserSchedulingEnabled: true,
+ DefaultTTL: defaultTTL,
+ MaxTTL: maxTTL,
+ })
+ if err != nil {
+ return xerrors.Errorf("set template schedule options: %w", err)
+ }
}
return nil
diff --git a/coderd/templates_test.go b/coderd/templates_test.go
index 4a5f181e5ea56..9ebb12c6a944d 100644
--- a/coderd/templates_test.go
+++ b/coderd/templates_test.go
@@ -430,6 +430,30 @@ func TestPatchTemplateMeta(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, updated.Icon, "")
})
+
+ t.Run("MaxTTLEnterpriseOnly", func(t *testing.T) {
+ t.Parallel()
+
+ client := coderdtest.New(t, nil)
+ user := coderdtest.CreateFirstUser(t, client)
+ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
+ template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
+ require.EqualValues(t, 0, template.MaxTTLMillis)
+ req := codersdk.UpdateTemplateMeta{
+ MaxTTLMillis: time.Hour.Milliseconds(),
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
+ defer cancel()
+
+ updated, err := client.UpdateTemplateMeta(ctx, template.ID, req)
+ require.NoError(t, err)
+ require.EqualValues(t, 0, updated.MaxTTLMillis)
+
+ template, err = client.Template(ctx, template.ID)
+ require.NoError(t, err)
+ require.EqualValues(t, 0, template.MaxTTLMillis)
+ })
}
func TestDeleteTemplate(t *testing.T) {
diff --git a/codersdk/templates.go b/codersdk/templates.go
index fed87a08cec56..f9f46e542c03a 100644
--- a/codersdk/templates.go
+++ b/codersdk/templates.go
@@ -28,8 +28,11 @@ type Template struct {
Description string `json:"description"`
Icon string `json:"icon"`
DefaultTTLMillis int64 `json:"default_ttl_ms"`
- CreatedByID uuid.UUID `json:"created_by_id" format:"uuid"`
- CreatedByName string `json:"created_by_name"`
+ // MaxTTLMillis is an enterprise feature. It's value is only used if your
+ // license is entitled to use the advanced template scheduling feature.
+ MaxTTLMillis int64 `json:"max_ttl_ms"`
+ CreatedByID uuid.UUID `json:"created_by_id" format:"uuid"`
+ CreatedByName string `json:"created_by_name"`
AllowUserCancelWorkspaceJobs bool `json:"allow_user_cancel_workspace_jobs"`
}
@@ -75,12 +78,16 @@ type UpdateTemplateACL struct {
}
type UpdateTemplateMeta struct {
- Name string `json:"name,omitempty" validate:"omitempty,template_name"`
- DisplayName string `json:"display_name,omitempty" validate:"omitempty,template_display_name"`
- Description string `json:"description,omitempty"`
- Icon string `json:"icon,omitempty"`
- DefaultTTLMillis int64 `json:"default_ttl_ms,omitempty"`
- AllowUserCancelWorkspaceJobs bool `json:"allow_user_cancel_workspace_jobs,omitempty"`
+ Name string `json:"name,omitempty" validate:"omitempty,template_name"`
+ DisplayName string `json:"display_name,omitempty" validate:"omitempty,template_display_name"`
+ Description string `json:"description,omitempty"`
+ Icon string `json:"icon,omitempty"`
+ DefaultTTLMillis int64 `json:"default_ttl_ms,omitempty"`
+ // MaxTTLMillis 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.
+ MaxTTLMillis int64 `json:"max_ttl_ms,omitempty"`
+ AllowUserCancelWorkspaceJobs bool `json:"allow_user_cancel_workspace_jobs,omitempty"`
}
type TemplateExample struct {
diff --git a/docs/admin/audit-logs.md b/docs/admin/audit-logs.md
index 762b2f3762ca7..a43ba302f8f96 100644
--- a/docs/admin/audit-logs.md
+++ b/docs/admin/audit-logs.md
@@ -9,17 +9,17 @@ We track the following resources:
-| Resource | |
-| ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| APIKey
write | Field | Tracked |
---|
created_at | false |
expires_at | false |
hashed_secret | false |
id | false |
ip_address | false |
last_used | false |
lifetime_seconds | false |
login_type | false |
scope | false |
updated_at | false |
user_id | false |
|
-| Group
create, write, delete | Field | Tracked |
---|
avatar_url | true |
id | true |
members | true |
name | true |
organization_id | false |
quota_allowance | true |
|
-| GitSSHKey
create | Field | Tracked |
---|
created_at | false |
private_key | true |
public_key | true |
updated_at | false |
user_id | true |
|
-| License
create, delete | Field | Tracked |
---|
exp | true |
id | false |
jwt | false |
uploaded_at | true |
uuid | true |
|
-| Template
write, delete | Field | Tracked |
---|
active_version_id | true |
allow_user_cancel_workspace_jobs | true |
created_at | false |
created_by | true |
default_ttl | true |
deleted | false |
description | true |
display_name | true |
group_acl | true |
icon | true |
id | true |
is_private | true |
min_autostart_interval | true |
name | true |
organization_id | false |
provisioner | true |
updated_at | false |
user_acl | true |
|
-| TemplateVersion
create, write | Field | Tracked |
---|
created_at | false |
created_by | true |
id | true |
job_id | false |
name | true |
organization_id | false |
readme | true |
template_id | true |
updated_at | false |
|
-| User
create, write, delete | Field | Tracked |
---|
avatar_url | false |
created_at | false |
deleted | true |
email | true |
hashed_password | true |
id | true |
last_seen_at | false |
login_type | false |
rbac_roles | true |
status | true |
updated_at | false |
username | true |
|
-| Workspace
create, write, delete | Field | Tracked |
---|
autostart_schedule | true |
created_at | false |
deleted | false |
id | true |
last_used_at | false |
name | true |
organization_id | false |
owner_id | true |
template_id | true |
ttl | true |
updated_at | false |
|
-| WorkspaceBuild
start, stop | Field | Tracked |
---|
build_number | false |
created_at | false |
daily_cost | false |
deadline | false |
id | false |
initiator_id | false |
job_id | false |
provisioner_state | false |
reason | false |
template_version_id | true |
transition | false |
updated_at | false |
workspace_id | false |
|
+| Resource | |
+| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| APIKey
write | Field | Tracked |
---|
created_at | false |
expires_at | false |
hashed_secret | false |
id | false |
ip_address | false |
last_used | false |
lifetime_seconds | false |
login_type | false |
scope | false |
updated_at | false |
user_id | false |
|
+| Group
create, write, delete | Field | Tracked |
---|
avatar_url | true |
id | true |
members | true |
name | true |
organization_id | false |
quota_allowance | true |
|
+| GitSSHKey
create | Field | Tracked |
---|
created_at | false |
private_key | true |
public_key | true |
updated_at | false |
user_id | true |
|
+| License
create, delete | Field | Tracked |
---|
exp | true |
id | false |
jwt | false |
uploaded_at | true |
uuid | true |
|
+| Template
write, delete | Field | Tracked |
---|
active_version_id | true |
allow_user_cancel_workspace_jobs | true |
created_at | false |
created_by | true |
default_ttl | true |
deleted | false |
description | true |
display_name | true |
group_acl | true |
icon | true |
id | true |
is_private | true |
max_ttl | true |
min_autostart_interval | true |
name | true |
organization_id | false |
provisioner | true |
updated_at | false |
user_acl | true |
|
+| TemplateVersion
create, write | Field | Tracked |
---|
created_at | false |
created_by | true |
id | true |
job_id | false |
name | true |
organization_id | false |
readme | true |
template_id | true |
updated_at | false |
|
+| User
create, write, delete | Field | Tracked |
---|
avatar_url | false |
created_at | false |
deleted | true |
email | true |
hashed_password | true |
id | true |
last_seen_at | false |
login_type | false |
rbac_roles | true |
status | true |
updated_at | false |
username | true |
|
+| Workspace
create, write, delete | Field | Tracked |
---|
autostart_schedule | true |
created_at | false |
deleted | false |
id | true |
last_used_at | false |
name | true |
organization_id | false |
owner_id | true |
template_id | true |
ttl | true |
updated_at | false |
|
+| WorkspaceBuild
start, stop | Field | Tracked |
---|
build_number | false |
created_at | false |
daily_cost | false |
deadline | false |
id | false |
initiator_id | false |
job_id | false |
provisioner_state | false |
reason | false |
template_version_id | true |
transition | false |
updated_at | false |
workspace_id | false |
|
diff --git a/docs/api/schemas.md b/docs/api/schemas.md
index 948a0e6304197..3c671f08b8dda 100644
--- a/docs/api/schemas.md
+++ b/docs/api/schemas.md
@@ -4349,6 +4349,7 @@ Parameter represents a set value for the scope.
"display_name": "string",
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
+ "max_ttl_ms": 0,
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"provisioner": "terraform",
@@ -4358,24 +4359,25 @@ Parameter represents a set value for the scope.
### 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_cancel_workspace_jobs` | boolean | false | | |
-| `build_time_stats` | [codersdk.TemplateBuildTimeStats](#codersdktemplatebuildtimestats) | false | | |
-| `created_at` | string | false | | |
-| `created_by_id` | string | false | | |
-| `created_by_name` | string | false | | |
-| `default_ttl_ms` | integer | false | | |
-| `description` | string | false | | |
-| `display_name` | string | false | | |
-| `icon` | string | false | | |
-| `id` | string | false | | |
-| `name` | string | false | | |
-| `organization_id` | string | false | | |
-| `provisioner` | string | false | | |
-| `updated_at` | string | false | | |
+| Name | Type | Required | Restrictions | Description |
+| ---------------------------------- | ------------------------------------------------------------------ | -------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
+| `active_user_count` | integer | false | | Active user count is set to -1 when loading. |
+| `active_version_id` | string | false | | |
+| `allow_user_cancel_workspace_jobs` | boolean | false | | |
+| `build_time_stats` | [codersdk.TemplateBuildTimeStats](#codersdktemplatebuildtimestats) | false | | |
+| `created_at` | string | false | | |
+| `created_by_id` | string | false | | |
+| `created_by_name` | string | false | | |
+| `default_ttl_ms` | integer | false | | |
+| `description` | string | false | | |
+| `display_name` | string | false | | |
+| `icon` | string | false | | |
+| `id` | string | false | | |
+| `max_ttl_ms` | integer | false | | Max ttl ms is an enterprise feature. It's value is only used if your license is entitled to use the advanced template scheduling feature. |
+| `name` | string | false | | |
+| `organization_id` | string | false | | |
+| `provisioner` | string | false | | |
+| `updated_at` | string | false | | |
#### Enumerated Values
diff --git a/docs/api/templates.md b/docs/api/templates.md
index b48b805d7c58e..5986990f4cea5 100644
--- a/docs/api/templates.md
+++ b/docs/api/templates.md
@@ -118,6 +118,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templat
"display_name": "string",
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
+ "max_ttl_ms": 0,
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"provisioner": "terraform",
@@ -136,28 +137,29 @@ 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_cancel_workspace_jobs` | boolean | false | | |
-| `» build_time_stats` | [codersdk.TemplateBuildTimeStats](schemas.md#codersdktemplatebuildtimestats) | false | | |
-| `»» [any property]` | [codersdk.TransitionStats](schemas.md#codersdktransitionstats) | false | | |
-| `»»» p50` | integer | false | | |
-| `»»» p95` | integer | false | | |
-| `» created_at` | string(date-time) | false | | |
-| `» created_by_id` | string(uuid) | false | | |
-| `» created_by_name` | string | false | | |
-| `» default_ttl_ms` | integer | false | | |
-| `» description` | string | false | | |
-| `» display_name` | string | false | | |
-| `» icon` | string | false | | |
-| `» id` | string(uuid) | false | | |
-| `» name` | string | false | | |
-| `» organization_id` | string(uuid) | false | | |
-| `» provisioner` | string | false | | |
-| `» updated_at` | string(date-time) | false | | |
+| Name | Type | Required | Restrictions | Description |
+| ------------------------------------ | ---------------------------------------------------------------------------- | -------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
+| `[array item]` | array | false | | |
+| `» active_user_count` | integer | false | | Active user count is set to -1 when loading. |
+| `» active_version_id` | string(uuid) | false | | |
+| `» allow_user_cancel_workspace_jobs` | boolean | false | | |
+| `» build_time_stats` | [codersdk.TemplateBuildTimeStats](schemas.md#codersdktemplatebuildtimestats) | false | | |
+| `»» [any property]` | [codersdk.TransitionStats](schemas.md#codersdktransitionstats) | false | | |
+| `»»» p50` | integer | false | | |
+| `»»» p95` | integer | false | | |
+| `» created_at` | string(date-time) | false | | |
+| `» created_by_id` | string(uuid) | false | | |
+| `» created_by_name` | string | false | | |
+| `» default_ttl_ms` | integer | false | | |
+| `» description` | string | false | | |
+| `» display_name` | string | false | | |
+| `» icon` | string | false | | |
+| `» id` | string(uuid) | false | | |
+| `» max_ttl_ms` | integer | false | | Max ttl ms is an enterprise feature. It's value is only used if your license is entitled to use the advanced template scheduling feature. |
+| `» name` | string | false | | |
+| `» organization_id` | string(uuid) | false | | |
+| `» provisioner` | string | false | | |
+| `» updated_at` | string(date-time) | false | | |
#### Enumerated Values
@@ -238,6 +240,7 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/templa
"display_name": "string",
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
+ "max_ttl_ms": 0,
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"provisioner": "terraform",
@@ -360,6 +363,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templat
"display_name": "string",
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
+ "max_ttl_ms": 0,
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"provisioner": "terraform",
@@ -678,6 +682,7 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template} \
"display_name": "string",
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
+ "max_ttl_ms": 0,
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"provisioner": "terraform",
@@ -783,6 +788,7 @@ curl -X PATCH http://coder-server:8080/api/v2/templates/{template} \
"display_name": "string",
"icon": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
+ "max_ttl_ms": 0,
"name": "string",
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
"provisioner": "terraform",
diff --git a/enterprise/coderd/provisionerdaemons.go b/enterprise/coderd/provisionerdaemons.go
index 38ee1a0b783fe..9cfaa2c9e3f3a 100644
--- a/enterprise/coderd/provisionerdaemons.go
+++ b/enterprise/coderd/provisionerdaemons.go
@@ -308,7 +308,7 @@ type enterpriseTemplateScheduleStore struct{}
var _ provisionerdserver.TemplateScheduleStore = &enterpriseTemplateScheduleStore{}
-func (s *enterpriseTemplateScheduleStore) GetTemplateScheduleOptions(ctx context.Context, db database.Store, templateID uuid.UUID) (provisionerdserver.TemplateScheduleOptions, error) {
+func (*enterpriseTemplateScheduleStore) GetTemplateScheduleOptions(ctx context.Context, db database.Store, templateID uuid.UUID) (provisionerdserver.TemplateScheduleOptions, error) {
tpl, err := db.GetTemplateByID(ctx, templateID)
if err != nil {
return provisionerdserver.TemplateScheduleOptions{}, err
@@ -321,3 +321,12 @@ func (s *enterpriseTemplateScheduleStore) GetTemplateScheduleOptions(ctx context
MaxTTL: time.Duration(tpl.MaxTTL),
}, nil
}
+
+func (*enterpriseTemplateScheduleStore) SetTemplateScheduleOptions(ctx context.Context, db database.Store, tpl database.Template, opts provisionerdserver.TemplateScheduleOptions) (database.Template, error) {
+ return db.UpdateTemplateScheduleByID(ctx, database.UpdateTemplateScheduleByIDParams{
+ ID: tpl.ID,
+ UpdatedAt: database.Now(),
+ DefaultTTL: int64(opts.DefaultTTL),
+ MaxTTL: int64(opts.MaxTTL),
+ })
+}
diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts
index c7159fb50a8f8..b17146287b780 100644
--- a/site/src/api/typesGenerated.ts
+++ b/site/src/api/typesGenerated.ts
@@ -698,6 +698,7 @@ export interface Template {
readonly description: string
readonly icon: string
readonly default_ttl_ms: number
+ readonly max_ttl_ms: number
readonly created_by_id: string
readonly created_by_name: string
readonly allow_user_cancel_workspace_jobs: boolean
@@ -838,6 +839,7 @@ export interface UpdateTemplateMeta {
readonly description?: string
readonly icon?: string
readonly default_ttl_ms?: number
+ readonly max_ttl_ms?: number
readonly allow_user_cancel_workspace_jobs?: boolean
}
@@ -1112,6 +1114,7 @@ export const Experiments: Experiment[] = ["authz_querier", "template_editor"]
// From codersdk/deployment.go
export type FeatureName =
+ | "advanced_template_scheduling"
| "appearance"
| "audit_log"
| "browser_only"
@@ -1122,6 +1125,7 @@ export type FeatureName =
| "template_rbac"
| "user_limit"
export const FeatureNames: FeatureName[] = [
+ "advanced_template_scheduling",
"appearance",
"audit_log",
"browser_only",
From ad64806901c3a897835232c3a86f02e129fcd2f4 Mon Sep 17 00:00:00 2001
From: Dean Sheather
Date: Mon, 20 Feb 2023 14:18:05 +0000
Subject: [PATCH 04/24] working API and dashboard
---
coderd/apidoc/docs.go | 4 ++
coderd/apidoc/swagger.json | 4 ++
coderd/database/dbfake/databasefake.go | 1 -
coderd/database/dbgen/generator.go | 1 -
...100_workspace_build_max_deadline.down.sql} | 0
...00100_workspace_build_max_deadline.up.sql} | 0
coderd/database/modelqueries.go | 1 +
coderd/database/queries.sql.go | 5 +-
coderd/database/queries/templates.sql | 3 +-
coderd/templates.go | 51 +++++++++++++++----
coderd/templates_test.go | 8 ++-
codersdk/organizations.go | 3 ++
docs/api/schemas.md | 2 +
docs/api/templates.md | 1 +
enterprise/coderd/templates_test.go | 39 ++++++++++++++
site/src/api/typesGenerated.ts | 1 +
site/src/i18n/en/createTemplatePage.json | 2 +
site/src/i18n/en/templateSettingsPage.json | 18 ++++---
.../CreateTemplatePage/CreateTemplateForm.tsx | 37 ++++++++++++--
.../TemplateSettingsForm.tsx | 42 +++++++++++++--
.../createTemplate/createTemplateXService.ts | 3 ++
21 files changed, 193 insertions(+), 33 deletions(-)
rename coderd/database/migrations/{000097_workspace_build_max_deadline.down.sql => 000100_workspace_build_max_deadline.down.sql} (100%)
rename coderd/database/migrations/{000097_workspace_build_max_deadline.up.sql => 000100_workspace_build_max_deadline.up.sql} (100%)
diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go
index b2f224135c8b2..865254d2f8c14 100644
--- a/coderd/apidoc/docs.go
+++ b/coderd/apidoc/docs.go
@@ -5759,6 +5759,10 @@ const docTemplate = `{
"description": "Icon is a relative path or external URL that specifies\nan icon to be displayed in the dashboard.",
"type": "string"
},
+ "max_ttl_ms": {
+ "description": "MaxTTLMillis allows optionally specifying the max lifetime for\nworkspaces created from this template.",
+ "type": "integer"
+ },
"name": {
"description": "Name is the name of the template.",
"type": "string"
diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json
index c777743e6d011..8397bd6fe9440 100644
--- a/coderd/apidoc/swagger.json
+++ b/coderd/apidoc/swagger.json
@@ -5107,6 +5107,10 @@
"description": "Icon is a relative path or external URL that specifies\nan icon to be displayed in the dashboard.",
"type": "string"
},
+ "max_ttl_ms": {
+ "description": "MaxTTLMillis allows optionally specifying the max lifetime for\nworkspaces created from this template.",
+ "type": "integer"
+ },
"name": {
"description": "Name is the name of the template.",
"type": "string"
diff --git a/coderd/database/dbfake/databasefake.go b/coderd/database/dbfake/databasefake.go
index bce496e205582..f7e8443987550 100644
--- a/coderd/database/dbfake/databasefake.go
+++ b/coderd/database/dbfake/databasefake.go
@@ -2615,7 +2615,6 @@ func (q *fakeQuerier) InsertTemplate(_ context.Context, arg database.InsertTempl
Provisioner: arg.Provisioner,
ActiveVersionID: arg.ActiveVersionID,
Description: arg.Description,
- DefaultTTL: arg.DefaultTTL,
CreatedBy: arg.CreatedBy,
UserACL: arg.UserACL,
GroupACL: arg.GroupACL,
diff --git a/coderd/database/dbgen/generator.go b/coderd/database/dbgen/generator.go
index cb8f52c06529a..11303a9a29793 100644
--- a/coderd/database/dbgen/generator.go
+++ b/coderd/database/dbgen/generator.go
@@ -60,7 +60,6 @@ func Template(t testing.TB, db database.Store, seed database.Template) database.
Provisioner: takeFirst(seed.Provisioner, database.ProvisionerTypeEcho),
ActiveVersionID: takeFirst(seed.ActiveVersionID, uuid.New()),
Description: takeFirst(seed.Description, namesgenerator.GetRandomName(1)),
- DefaultTTL: takeFirst(seed.DefaultTTL, 3600),
CreatedBy: takeFirst(seed.CreatedBy, uuid.New()),
Icon: takeFirst(seed.Icon, namesgenerator.GetRandomName(1)),
UserACL: seed.UserACL,
diff --git a/coderd/database/migrations/000097_workspace_build_max_deadline.down.sql b/coderd/database/migrations/000100_workspace_build_max_deadline.down.sql
similarity index 100%
rename from coderd/database/migrations/000097_workspace_build_max_deadline.down.sql
rename to coderd/database/migrations/000100_workspace_build_max_deadline.down.sql
diff --git a/coderd/database/migrations/000097_workspace_build_max_deadline.up.sql b/coderd/database/migrations/000100_workspace_build_max_deadline.up.sql
similarity index 100%
rename from coderd/database/migrations/000097_workspace_build_max_deadline.up.sql
rename to coderd/database/migrations/000100_workspace_build_max_deadline.up.sql
diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go
index 348555285ad03..a9bdfca75d46f 100644
--- a/coderd/database/modelqueries.go
+++ b/coderd/database/modelqueries.go
@@ -77,6 +77,7 @@ func (q *sqlQuerier) GetAuthorizedTemplates(ctx context.Context, arg GetTemplate
&i.GroupACL,
&i.DisplayName,
&i.AllowUserCancelWorkspaceJobs,
+ &i.MaxTTL,
); err != nil {
return nil, err
}
diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go
index 2278594709f16..03c7c583f1776 100644
--- a/coderd/database/queries.sql.go
+++ b/coderd/database/queries.sql.go
@@ -3331,7 +3331,6 @@ INSERT INTO
provisioner,
active_version_id,
description,
- default_ttl,
created_by,
icon,
user_acl,
@@ -3340,7 +3339,7 @@ INSERT INTO
allow_user_cancel_workspace_jobs
)
VALUES
- ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) RETURNING id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl
+ ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl
`
type InsertTemplateParams struct {
@@ -3352,7 +3351,6 @@ type InsertTemplateParams struct {
Provisioner ProvisionerType `db:"provisioner" json:"provisioner"`
ActiveVersionID uuid.UUID `db:"active_version_id" json:"active_version_id"`
Description string `db:"description" json:"description"`
- DefaultTTL int64 `db:"default_ttl" json:"default_ttl"`
CreatedBy uuid.UUID `db:"created_by" json:"created_by"`
Icon string `db:"icon" json:"icon"`
UserACL TemplateACL `db:"user_acl" json:"user_acl"`
@@ -3371,7 +3369,6 @@ func (q *sqlQuerier) InsertTemplate(ctx context.Context, arg InsertTemplateParam
arg.Provisioner,
arg.ActiveVersionID,
arg.Description,
- arg.DefaultTTL,
arg.CreatedBy,
arg.Icon,
arg.UserACL,
diff --git a/coderd/database/queries/templates.sql b/coderd/database/queries/templates.sql
index 30cccd40b033d..03b9c9bccc954 100644
--- a/coderd/database/queries/templates.sql
+++ b/coderd/database/queries/templates.sql
@@ -67,7 +67,6 @@ INSERT INTO
provisioner,
active_version_id,
description,
- default_ttl,
created_by,
icon,
user_acl,
@@ -76,7 +75,7 @@ INSERT INTO
allow_user_cancel_workspace_jobs
)
VALUES
- ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) RETURNING *;
+ ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING *;
-- name: UpdateTemplateActiveVersionByID :exec
UPDATE
diff --git a/coderd/templates.go b/coderd/templates.go
index 21d47b138ac16..e91334cbb7449 100644
--- a/coderd/templates.go
+++ b/coderd/templates.go
@@ -213,16 +213,31 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
return
}
- var ttl time.Duration
+ var (
+ defaultTTL time.Duration
+ maxTTL time.Duration
+ )
if createTemplate.DefaultTTLMillis != nil {
- ttl = time.Duration(*createTemplate.DefaultTTLMillis) * time.Millisecond
+ defaultTTL = time.Duration(*createTemplate.DefaultTTLMillis) * time.Millisecond
+ }
+ if createTemplate.MaxTTLMillis != nil {
+ maxTTL = time.Duration(*createTemplate.MaxTTLMillis) * time.Millisecond
+ }
+
+ var validErrs []codersdk.ValidationError
+ if defaultTTL < 0 {
+ validErrs = append(validErrs, codersdk.ValidationError{Field: "default_ttl_ms", Detail: "Must be a positive integer."})
+ }
+ if maxTTL < 0 {
+ validErrs = append(validErrs, codersdk.ValidationError{Field: "max_ttl_ms", Detail: "Must be a positive integer."})
+ }
+ if maxTTL != 0 && defaultTTL > maxTTL {
+ validErrs = append(validErrs, codersdk.ValidationError{Field: "default_ttl_ms", Detail: "Must be less than or equal to max_ttl_ms if max_ttl_ms is set."})
}
- if ttl < 0 {
+ if len(validErrs) > 0 {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
- Message: "Invalid create template request.",
- Validations: []codersdk.ValidationError{
- {Field: "default_ttl_ms", Detail: "Must be a positive integer."},
- },
+ Message: "Invalid create template request.",
+ Validations: validErrs,
})
return
}
@@ -245,7 +260,6 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
Provisioner: importJob.Provisioner,
ActiveVersionID: templateVersion.ID,
Description: createTemplate.Description,
- DefaultTTL: int64(ttl),
CreatedBy: apiKey.UserID,
UserACL: database.TemplateACL{},
GroupACL: database.TemplateACL{
@@ -259,6 +273,17 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
return xerrors.Errorf("insert template: %s", err)
}
+ if defaultTTL != 0 || maxTTL != 0 {
+ dbTemplate, err = (*api.TemplateScheduleStore.Load()).SetTemplateScheduleOptions(ctx, tx, dbTemplate, provisionerdserver.TemplateScheduleOptions{
+ UserSchedulingEnabled: true,
+ DefaultTTL: defaultTTL,
+ MaxTTL: maxTTL,
+ })
+ if err != nil {
+ return xerrors.Errorf("set template schedule options: %s", err)
+ }
+ }
+
templateAudit.New = dbTemplate
err = tx.UpdateTemplateVersionByID(ctx, database.UpdateTemplateVersionByIDParams{
@@ -453,6 +478,12 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
if req.DefaultTTLMillis < 0 {
validErrs = append(validErrs, codersdk.ValidationError{Field: "default_ttl_ms", Detail: "Must be a positive integer."})
}
+ if req.MaxTTLMillis < 0 {
+ validErrs = append(validErrs, codersdk.ValidationError{Field: "max_ttl_ms", Detail: "Must be a positive integer."})
+ }
+ if req.MaxTTLMillis != 0 && req.DefaultTTLMillis > req.MaxTTLMillis {
+ validErrs = append(validErrs, codersdk.ValidationError{Field: "default_ttl_ms", Detail: "Must be less than or equal to max_ttl_ms if max_ttl_ms is set."})
+ }
if len(validErrs) > 0 {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
@@ -469,7 +500,8 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
req.DisplayName == template.DisplayName &&
req.Icon == template.Icon &&
req.AllowUserCancelWorkspaceJobs == template.AllowUserCancelWorkspaceJobs &&
- req.DefaultTTLMillis == time.Duration(template.DefaultTTL).Milliseconds() {
+ req.DefaultTTLMillis == time.Duration(template.DefaultTTL).Milliseconds() &&
+ req.MaxTTLMillis == time.Duration(template.MaxTTL).Milliseconds() {
return nil
}
@@ -647,6 +679,7 @@ func (api *API) convertTemplate(
Description: template.Description,
Icon: template.Icon,
DefaultTTLMillis: time.Duration(template.DefaultTTL).Milliseconds(),
+ MaxTTLMillis: time.Duration(template.MaxTTL).Milliseconds(),
CreatedByID: template.CreatedBy,
CreatedByName: createdByName,
AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
diff --git a/coderd/templates_test.go b/coderd/templates_test.go
index 9ebb12c6a944d..092f6f75c9d99 100644
--- a/coderd/templates_test.go
+++ b/coderd/templates_test.go
@@ -440,7 +440,13 @@ func TestPatchTemplateMeta(t *testing.T) {
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
require.EqualValues(t, 0, template.MaxTTLMillis)
req := codersdk.UpdateTemplateMeta{
- MaxTTLMillis: time.Hour.Milliseconds(),
+ Name: template.Name,
+ DisplayName: template.DisplayName,
+ Description: template.Description,
+ Icon: template.Icon,
+ AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
+ DefaultTTLMillis: time.Hour.Milliseconds(),
+ MaxTTLMillis: (2 * time.Hour).Milliseconds(),
}
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
diff --git a/codersdk/organizations.go b/codersdk/organizations.go
index 5427b93e2006e..f3a30bf4f94fe 100644
--- a/codersdk/organizations.go
+++ b/codersdk/organizations.go
@@ -88,6 +88,9 @@ type CreateTemplateRequest struct {
// DefaultTTLMillis allows optionally specifying the default TTL
// for all workspaces created from this template.
DefaultTTLMillis *int64 `json:"default_ttl_ms,omitempty"`
+ // MaxTTLMillis allows optionally specifying the max lifetime for
+ // workspaces created from this template.
+ MaxTTLMillis *int64 `json:"max_ttl_ms,omitempty"`
// Allow users to cancel in-progress workspace jobs.
// *bool as the default value is "true".
diff --git a/docs/api/schemas.md b/docs/api/schemas.md
index 3c671f08b8dda..320b98693a6f6 100644
--- a/docs/api/schemas.md
+++ b/docs/api/schemas.md
@@ -953,6 +953,7 @@ CreateParameterRequest is a structure used to create a new parameter value for a
"description": "string",
"display_name": "string",
"icon": "string",
+ "max_ttl_ms": 0,
"name": "string",
"parameter_values": [
{
@@ -976,6 +977,7 @@ CreateParameterRequest is a structure used to create a new parameter value for a
| `description` | string | false | | Description is a description of what the template contains. It must be less than 128 bytes. |
| `display_name` | string | false | | Display name is the displayed name of the template. |
| `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 allows optionally specifying the max lifetime for workspaces created from this template. |
| `name` | string | true | | Name is the name of the template. |
| `parameter_values` | array of [codersdk.CreateParameterRequest](#codersdkcreateparameterrequest) | false | | Parameter values is a structure used to create a new parameter value for a scope.] |
| `template_version_id` | string | true | | Template version ID is an in-progress or completed job to use as an initial version of the template. |
diff --git a/docs/api/templates.md b/docs/api/templates.md
index 5986990f4cea5..39a40dfe1167c 100644
--- a/docs/api/templates.md
+++ b/docs/api/templates.md
@@ -192,6 +192,7 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/templa
"description": "string",
"display_name": "string",
"icon": "string",
+ "max_ttl_ms": 0,
"name": "string",
"parameter_values": [
{
diff --git a/enterprise/coderd/templates_test.go b/enterprise/coderd/templates_test.go
index 7c9303f188310..c379675edff2d 100644
--- a/enterprise/coderd/templates_test.go
+++ b/enterprise/coderd/templates_test.go
@@ -5,6 +5,7 @@ import (
"context"
"net/http"
"testing"
+ "time"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
@@ -21,6 +22,44 @@ import (
"github.com/coder/coder/testutil"
)
+func TestTemplates(t *testing.T) {
+ t.Parallel()
+
+ t.Run("SetMaxTTL", func(t *testing.T) {
+ t.Parallel()
+
+ client := coderdenttest.New(t, nil)
+ user := coderdtest.CreateFirstUser(t, client)
+ _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
+ Features: license.Features{
+ codersdk.FeatureAdvancedTemplateScheduling: 1,
+ },
+ })
+
+ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
+ template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
+ require.EqualValues(t, 0, template.MaxTTLMillis)
+
+ ctx, _ := testutil.Context(t)
+
+ updated, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
+ Name: template.Name,
+ DisplayName: template.DisplayName,
+ Description: template.Description,
+ Icon: template.Icon,
+ AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
+ DefaultTTLMillis: time.Hour.Milliseconds(),
+ MaxTTLMillis: (2 * time.Hour).Milliseconds(),
+ })
+ require.NoError(t, err)
+ require.Equal(t, 2*time.Hour, time.Duration(updated.MaxTTLMillis)*time.Millisecond)
+
+ template, err = client.Template(ctx, template.ID)
+ require.NoError(t, err)
+ require.Equal(t, 2*time.Hour, time.Duration(template.MaxTTLMillis)*time.Millisecond)
+ })
+}
+
func TestTemplateACL(t *testing.T) {
t.Parallel()
diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts
index b17146287b780..8957e39081a8a 100644
--- a/site/src/api/typesGenerated.ts
+++ b/site/src/api/typesGenerated.ts
@@ -179,6 +179,7 @@ export interface CreateTemplateRequest {
readonly template_version_id: string
readonly parameter_values?: CreateParameterRequest[]
readonly default_ttl_ms?: number
+ readonly max_ttl_ms?: number
readonly allow_user_cancel_workspace_jobs?: boolean
}
diff --git a/site/src/i18n/en/createTemplatePage.json b/site/src/i18n/en/createTemplatePage.json
index 575c492aa0fe3..996adb276e473 100644
--- a/site/src/i18n/en/createTemplatePage.json
+++ b/site/src/i18n/en/createTemplatePage.json
@@ -27,10 +27,12 @@
"description": "Description",
"icon": "Icon",
"autoStop": "Auto-stop default",
+ "maxTTL": "Maximum lifetime of workspaces",
"allowUsersToCancel": "Allow users to cancel in-progress workspace jobs"
},
"helperText": {
"autoStop": "Time in hours",
+ "maxTTL": "Time in hours",
"allowUsersToCancel": "If checked, users may be able to corrupt their workspace."
},
"upload": {
diff --git a/site/src/i18n/en/templateSettingsPage.json b/site/src/i18n/en/templateSettingsPage.json
index f738567b50756..edcaf469a6726 100644
--- a/site/src/i18n/en/templateSettingsPage.json
+++ b/site/src/i18n/en/templateSettingsPage.json
@@ -5,14 +5,20 @@
"descriptionLabel": "Description",
"descriptionMaxError": "Please enter a description that is less than or equal to 128 characters.",
"defaultTtlLabel": "Auto-stop default",
+ "maxTtlLabel": "Maximum lifetime for started workspaces",
"iconLabel": "Icon",
"formAriaLabel": "Template settings form",
"selectEmoji": "Select emoji",
- "ttlMaxError": "Please enter a limit that is less than or equal to 168 hours (7 days).",
- "ttlMinError": "Default time until auto-stop must not be less than 0.",
- "ttlHelperText_zero": "Workspaces created from this template will run until stopped manually.",
- "ttlHelperText_one": "Workspaces created from this template will default to stopping after {{count}} hour.",
- "ttlHelperText_other": "Workspaces created from this template will default to stopping after {{count}} hours.",
+ "defaultTTLMaxError": "Please enter a limit that is less than or equal to 168 hours (7 days).",
+ "defaultTTLMinError": "Default time until auto-stop must not be less than 0.",
+ "defaultTTLHelperText_zero": "Workspaces created from this template will run until stopped manually.",
+ "defaultTTLHelperText_one": "Workspaces created from this template will default to stopping after {{count}} hour.",
+ "defaultTTLHelperText_other": "Workspaces created from this template will default to stopping after {{count}} hours.",
+ "maxTTLMaxError": "Please enter a limit that is less than or equal to 168 hours (7 days).",
+ "maxTTLMinError": "Maximum time until auto-stop must not be less than 0.",
+ "maxTTLHelperText_zero": "Workspaces created from this template may run indefinitely.",
+ "maxTTLHelperText_one": "Workspaces created from this template must stop within {{count}} hour of starting.",
+ "maxTTLHelperText_other": "Workspaces created from this template must stop within {{count}} hours of starting.",
"allowUserCancelWorkspaceJobsLabel": "Allow users to cancel in-progress workspace jobs.",
"allowUserCancelWorkspaceJobsNotice": "Depending on your template, canceling builds may leave workspaces in an unhealthy state. This option isn't recommended for most use cases.",
"allowUsersCancelHelperText": "If checked, users may be able to corrupt their workspace.",
@@ -26,7 +32,7 @@
},
"schedule": {
"title": "Schedule",
- "description": "Define when workspaces created from this template automatically stop."
+ "description": "Define when workspaces created from this template are stopped."
},
"operations": {
"title": "Operations",
diff --git a/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx b/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx
index 48fe9dd55f8d0..1a274b232b680 100644
--- a/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx
+++ b/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx
@@ -23,6 +23,8 @@ import * as Yup from "yup"
import { WorkspaceBuildLogs } from "components/WorkspaceBuildLogs/WorkspaceBuildLogs"
import { HelpTooltip, HelpTooltipText } from "components/Tooltips/HelpTooltip"
import { LazyIconField } from "components/IconField/LazyIconField"
+import { Maybe } from "components/Conditionals/Maybe"
+import { useDashboard } from "components/Dashboard/DashboardProvider"
const validationSchema = Yup.object({
name: nameValidator("Name"),
@@ -30,6 +32,7 @@ const validationSchema = Yup.object({
description: Yup.string().optional(),
icon: Yup.string().optional(),
default_ttl_hours: Yup.number(),
+ max_ttl_hours: Yup.number(),
allow_user_cancel_workspace_jobs: Yup.boolean(),
parameter_values_by_name: Yup.object().optional(),
})
@@ -40,17 +43,27 @@ const defaultInitialValues: CreateTemplateData = {
description: "",
icon: "",
default_ttl_hours: 24,
+ // max_ttl is an enterprise-only feature, and the server ignores the value if
+ // you are not licensed. We hide the form value based on entitlements.
+ max_ttl_hours: 24 * 7,
allow_user_cancel_workspace_jobs: false,
parameter_values_by_name: undefined,
}
-const getInitialValues = (starterTemplate?: TemplateExample) => {
+const getInitialValues = (canSetMaxTTL: boolean, starterTemplate?: TemplateExample) => {
+ let initialValues = defaultInitialValues
+ if (!canSetMaxTTL) {
+ initialValues = {
+ ...initialValues,
+ max_ttl_hours: 0,
+ }
+ }
if (!starterTemplate) {
- return defaultInitialValues
+ return initialValues
}
return {
- ...defaultInitialValues,
+ ...initialValues,
name: starterTemplate.id,
display_name: starterTemplate.name,
icon: starterTemplate.icon,
@@ -83,8 +96,11 @@ export const CreateTemplateForm: FC = ({
}) => {
const styles = useStyles()
const formFooterStyles = useFormFooterStyles()
+ const { entitlements } = useDashboard()
+ const canSetMaxTTL = entitlements.features["advanced_template_scheduling"].enabled
+
const form = useFormik({
- initialValues: getInitialValues(starterTemplate),
+ initialValues: getInitialValues(canSetMaxTTL, starterTemplate),
validationSchema,
onSubmit,
})
@@ -188,6 +204,19 @@ export const CreateTemplateForm: FC = ({
type="number"
helperText={t("form.helperText.autoStop")}
/>
+
+
+
+
diff --git a/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx
index 84bdbe314b0fd..46a0c42ed96e0 100644
--- a/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx
+++ b/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx
@@ -23,14 +23,15 @@ import { Stack } from "components/Stack/Stack"
import Checkbox from "@material-ui/core/Checkbox"
import { HelpTooltip, HelpTooltipText } from "components/Tooltips/HelpTooltip"
import { makeStyles } from "@material-ui/core/styles"
+import { useDashboard } from "components/Dashboard/DashboardProvider"
-const TTLHelperText = ({ ttl }: { ttl?: number }) => {
++const TTLHelperText = ({ ttl, translationName }: { ttl?: number, translationName: string }) => {
const { t } = useTranslation("templateSettingsPage")
const count = typeof ttl !== "number" ? 0 : ttl
return (
// no helper text if ttl is negative - error will show once field is considered touched
= 0}>
- {t("ttlHelperText", { count })}
++ {t(translationName, { count })}
)
}
@@ -53,10 +54,17 @@ export const getValidationSchema = (): Yup.AnyObjectSchema =>
),
default_ttl_ms: Yup.number()
.integer()
- .min(0, i18next.t("ttlMinError", { ns: "templateSettingsPage" }))
+ .min(0, i18next.t("defaultTTLMinError", { ns: "templateSettingsPage" }))
.max(
24 * MAX_TTL_DAYS /* 7 days in hours */,
- i18next.t("ttlMaxError", { ns: "templateSettingsPage" }),
+ i18next.t("defaultTTLMaxError", { ns: "templateSettingsPage" }),
+ ),
+ max_ttl_ms: Yup.number()
+ .integer()
+ .min(0, i18next.t("maxTTLMinError", { ns: "templateSettingsPage" }))
+ .max(
+ 24 * MAX_TTL_DAYS /* 7 days in hours */,
+ i18next.t("maxTTLMaxError", { ns: "templateSettingsPage" }),
),
allow_user_cancel_workspace_jobs: Yup.boolean(),
})
@@ -79,6 +87,9 @@ export const TemplateSettingsForm: FC = ({
isSubmitting,
initialTouched,
}) => {
+ const { entitlements } = useDashboard()
+ const canSetMaxTTL = entitlements.features["advanced_template_scheduling"].enabled
+
const validationSchema = getValidationSchema()
const form: FormikContextType =
useFormik({
@@ -88,6 +99,9 @@ export const TemplateSettingsForm: FC = ({
description: template.description,
// on display, convert from ms => hours
default_ttl_ms: template.default_ttl_ms / MS_HOUR_CONVERSION,
+ // the API ignores this value, but to avoid tripping up validation set
+ // it to zero if the user can't set the field.
+ max_ttl_ms: canSetMaxTTL ? template.max_ttl_ms / MS_HOUR_CONVERSION : 0,
icon: template.icon,
allow_user_cancel_workspace_jobs:
template.allow_user_cancel_workspace_jobs,
@@ -100,6 +114,9 @@ export const TemplateSettingsForm: FC = ({
default_ttl_ms: formData.default_ttl_ms
? formData.default_ttl_ms * MS_HOUR_CONVERSION
: undefined,
+ max_ttl_ms: formData.max_ttl_ms
+ ? formData.max_ttl_ms * MS_HOUR_CONVERSION
+ : undefined,
})
},
initialTouched,
@@ -172,7 +189,7 @@ export const TemplateSettingsForm: FC = ({
,
+ ,
)}
disabled={isSubmitting}
fullWidth
@@ -181,6 +198,21 @@ export const TemplateSettingsForm: FC = ({
variant="outlined"
type="number"
/>
+
+
+ ,
+ )}
+ disabled={isSubmitting}
+ fullWidth
+ inputProps={{ min: 0, step: 1 }}
+ label={t("defaultTtlLabel")}
+ variant="outlined"
+ type="number"
+ />
+
}
@@ -372,6 +373,7 @@ export const createTemplateMachine =
const {
default_ttl_hours,
+ max_ttl_hours,
parameter_values_by_name,
...safeTemplateData
} = templateData
@@ -379,6 +381,7 @@ export const createTemplateMachine =
return createTemplate(organizationId, {
...safeTemplateData,
default_ttl_ms: templateData.default_ttl_hours * 60 * 60 * 1000, // Convert hours to ms
+ max_ttl_ms: templateData.max_ttl_hours * 60 * 60 * 1000, // Convert hours to ms
template_version_id: version.id,
})
},
From 96fd84050f5adc41de667ec615fe248f52f0faeb Mon Sep 17 00:00:00 2001
From: Dean Sheather
Date: Mon, 20 Feb 2023 15:25:39 +0000
Subject: [PATCH 05/24] working CLI
---
cli/templateedit.go | 29 ++-
cli/templateedit_test.go | 211 ++++++++++++++++++
.../coder_templates_edit_--help.golden | 15 +-
.../CreateTemplatePage/CreateTemplateForm.tsx | 8 +-
.../TemplateSettingsForm.tsx | 27 ++-
.../TemplateSettingsPage.test.tsx | 84 ++++++-
site/src/testHelpers/entities.ts | 1 +
7 files changed, 348 insertions(+), 27 deletions(-)
diff --git a/cli/templateedit.go b/cli/templateedit.go
index 5a99634511520..1e487fa8000c2 100644
--- a/cli/templateedit.go
+++ b/cli/templateedit.go
@@ -2,6 +2,7 @@ package cli
import (
"fmt"
+ "net/http"
"time"
"github.com/spf13/cobra"
@@ -18,6 +19,7 @@ func templateEdit() *cobra.Command {
description string
icon string
defaultTTL time.Duration
+ maxTTL time.Duration
allowUserCancelWorkspaceJobs bool
)
@@ -30,6 +32,21 @@ func templateEdit() *cobra.Command {
if err != nil {
return xerrors.Errorf("create client: %w", err)
}
+
+ if maxTTL != 0 {
+ entitlements, err := client.Entitlements(cmd.Context())
+ var sdkErr *codersdk.Error
+ if xerrors.As(err, &sdkErr) && sdkErr.StatusCode() == http.StatusNotFound {
+ return xerrors.Errorf("your deployment appears to be an AGPL deployment, so you cannot set --max-ttl")
+ } else if err != nil {
+ return xerrors.Errorf("get entitlements: %w", err)
+ }
+
+ if !entitlements.Features[codersdk.FeatureAdvancedTemplateScheduling].Enabled {
+ return xerrors.Errorf("your license is not entitled to use advanced template scheduling, so you cannot set --max-ttl")
+ }
+ }
+
organization, err := CurrentOrganization(cmd, client)
if err != nil {
return xerrors.Errorf("get current organization: %w", err)
@@ -46,6 +63,7 @@ func templateEdit() *cobra.Command {
Description: description,
Icon: icon,
DefaultTTLMillis: defaultTTL.Milliseconds(),
+ MaxTTLMillis: maxTTL.Milliseconds(),
AllowUserCancelWorkspaceJobs: allowUserCancelWorkspaceJobs,
}
@@ -58,11 +76,12 @@ func templateEdit() *cobra.Command {
},
}
- cmd.Flags().StringVarP(&name, "name", "", "", "Edit the template name")
- cmd.Flags().StringVarP(&displayName, "display-name", "", "", "Edit the template display name")
- cmd.Flags().StringVarP(&description, "description", "", "", "Edit the template description")
- cmd.Flags().StringVarP(&icon, "icon", "", "", "Edit the template icon path")
- cmd.Flags().DurationVarP(&defaultTTL, "default-ttl", "", 0, "Edit the template default time before shutdown - workspaces created from this template to this value.")
+ cmd.Flags().StringVarP(&name, "name", "", "", "Edit the template name.")
+ cmd.Flags().StringVarP(&displayName, "display-name", "", "", "Edit the template display name.")
+ cmd.Flags().StringVarP(&description, "description", "", "", "Edit the template description.")
+ cmd.Flags().StringVarP(&icon, "icon", "", "", "Edit the template icon path.")
+ cmd.Flags().DurationVarP(&defaultTTL, "default-ttl", "", 0, "Edit the template default time before shutdown - workspaces created from this template default to this value.")
+ cmd.Flags().DurationVarP(&maxTTL, "max-ttl", "", 0, "Edit the template maximum time before shutdown - workspaces created from this template must shutdown within the given duration after starting. This is an enterprise-only feature.")
cmd.Flags().BoolVarP(&allowUserCancelWorkspaceJobs, "allow-user-cancel-workspace-jobs", "", true, "Allow users to cancel in-progress workspace jobs.")
cliui.AllowSkipPrompt(cmd)
diff --git a/cli/templateedit_test.go b/cli/templateedit_test.go
index 28fe0edabf8c5..4361985feaddd 100644
--- a/cli/templateedit_test.go
+++ b/cli/templateedit_test.go
@@ -1,8 +1,17 @@
package cli_test
import (
+ "bytes"
"context"
+ "encoding/json"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "net/http/httputil"
+ "net/url"
"strconv"
+ "strings"
+ "sync/atomic"
"testing"
"time"
@@ -11,6 +20,7 @@ import (
"github.com/coder/coder/cli/clitest"
"github.com/coder/coder/coderd/coderdtest"
+ "github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/testutil"
)
@@ -230,4 +240,205 @@ func TestTemplateEdit(t *testing.T) {
assert.Equal(t, "", updated.Icon)
assert.Equal(t, "", updated.DisplayName)
})
+ t.Run("MaxTTL", func(t *testing.T) {
+ t.Parallel()
+ t.Run("BlockedAGPL", func(t *testing.T) {
+ t.Parallel()
+ client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
+ user := coderdtest.CreateFirstUser(t, client)
+ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
+ _ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
+ template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
+ ctr.DefaultTTLMillis = nil
+ ctr.MaxTTLMillis = nil
+ })
+
+ // Test the cli command.
+ cmdArgs := []string{
+ "templates",
+ "edit",
+ template.Name,
+ "--max-ttl", "1h",
+ }
+ cmd, root := clitest.New(t, cmdArgs...)
+ clitest.SetupConfig(t, client, root)
+
+ ctx, _ := testutil.Context(t)
+ err := cmd.ExecuteContext(ctx)
+ require.Error(t, err)
+ require.ErrorContains(t, err, "appears to be an AGPL deployment")
+
+ // Assert that the template metadata did not change.
+ updated, err := client.Template(context.Background(), template.ID)
+ require.NoError(t, err)
+ assert.Equal(t, template.Name, updated.Name)
+ assert.Equal(t, template.Description, updated.Description)
+ assert.Equal(t, template.Icon, updated.Icon)
+ assert.Equal(t, template.DisplayName, updated.DisplayName)
+ assert.Equal(t, template.DefaultTTLMillis, updated.DefaultTTLMillis)
+ assert.Equal(t, template.MaxTTLMillis, updated.MaxTTLMillis)
+ })
+
+ t.Run("BlockedNotEntitled", func(t *testing.T) {
+ t.Parallel()
+ client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
+ user := coderdtest.CreateFirstUser(t, client)
+ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
+ _ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
+ template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
+ ctr.DefaultTTLMillis = nil
+ ctr.MaxTTLMillis = nil
+ })
+
+ // Make a proxy server that will return a valid entitlements
+ // response, but without advanced scheduling entitlement.
+ proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path == "/api/v2/entitlements" {
+ res := codersdk.Entitlements{
+ Features: map[codersdk.FeatureName]codersdk.Feature{},
+ Warnings: []string{},
+ Errors: []string{},
+ HasLicense: true,
+ Trial: true,
+ RequireTelemetry: false,
+ Experimental: false,
+ }
+ for _, feature := range codersdk.FeatureNames {
+ res.Features[feature] = codersdk.Feature{
+ Entitlement: codersdk.EntitlementNotEntitled,
+ Enabled: false,
+ Limit: nil,
+ Actual: nil,
+ }
+ }
+ httpapi.Write(r.Context(), w, http.StatusOK, res)
+ return
+ }
+
+ // Otherwise, proxy the request to the real API server.
+ httputil.NewSingleHostReverseProxy(client.URL).ServeHTTP(w, r)
+ }))
+ defer proxy.Close()
+
+ // Create a new client that uses the proxy server.
+ proxyURL, err := url.Parse(proxy.URL)
+ require.NoError(t, err)
+ proxyClient := codersdk.New(proxyURL)
+ proxyClient.SetSessionToken(client.SessionToken())
+
+ // Test the cli command.
+ cmdArgs := []string{
+ "templates",
+ "edit",
+ template.Name,
+ "--max-ttl", "1h",
+ }
+ cmd, root := clitest.New(t, cmdArgs...)
+ clitest.SetupConfig(t, proxyClient, root)
+
+ ctx, _ := testutil.Context(t)
+ err = cmd.ExecuteContext(ctx)
+ require.Error(t, err)
+ require.ErrorContains(t, err, "license is not entitled")
+
+ // Assert that the template metadata did not change.
+ updated, err := client.Template(context.Background(), template.ID)
+ require.NoError(t, err)
+ assert.Equal(t, template.Name, updated.Name)
+ assert.Equal(t, template.Description, updated.Description)
+ assert.Equal(t, template.Icon, updated.Icon)
+ assert.Equal(t, template.DisplayName, updated.DisplayName)
+ assert.Equal(t, template.DefaultTTLMillis, updated.DefaultTTLMillis)
+ assert.Equal(t, template.MaxTTLMillis, updated.MaxTTLMillis)
+ })
+ t.Run("Entitled", func(t *testing.T) {
+ t.Parallel()
+ client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
+ user := coderdtest.CreateFirstUser(t, client)
+ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
+ _ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
+ template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
+ ctr.DefaultTTLMillis = nil
+ ctr.MaxTTLMillis = nil
+ })
+
+ // Make a proxy server that will return a valid entitlements
+ // response, including a valid advanced scheduling entitlement.
+ var updateTemplateCalled int64
+ proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path == "/api/v2/entitlements" {
+ res := codersdk.Entitlements{
+ Features: map[codersdk.FeatureName]codersdk.Feature{},
+ Warnings: []string{},
+ Errors: []string{},
+ HasLicense: true,
+ Trial: true,
+ RequireTelemetry: false,
+ Experimental: false,
+ }
+ for _, feature := range codersdk.FeatureNames {
+ var one int64 = 1
+ res.Features[feature] = codersdk.Feature{
+ Entitlement: codersdk.EntitlementNotEntitled,
+ Enabled: true,
+ Limit: &one,
+ Actual: &one,
+ }
+ }
+ httpapi.Write(r.Context(), w, http.StatusOK, res)
+ return
+ }
+ if strings.HasPrefix(r.URL.Path, "/api/v2/templates/") {
+ body, err := io.ReadAll(r.Body)
+ require.NoError(t, err)
+ _ = r.Body.Close()
+
+ var req codersdk.UpdateTemplateMeta
+ err = json.Unmarshal(body, &req)
+ require.NoError(t, err)
+ assert.Equal(t, time.Hour.Milliseconds(), req.MaxTTLMillis)
+
+ r.Body = io.NopCloser(bytes.NewReader(body))
+ atomic.AddInt64(&updateTemplateCalled, 1)
+ // We still want to call the real route.
+ }
+
+ // Otherwise, proxy the request to the real API server.
+ httputil.NewSingleHostReverseProxy(client.URL).ServeHTTP(w, r)
+ }))
+ defer proxy.Close()
+
+ // Create a new client that uses the proxy server.
+ proxyURL, err := url.Parse(proxy.URL)
+ require.NoError(t, err)
+ proxyClient := codersdk.New(proxyURL)
+ proxyClient.SetSessionToken(client.SessionToken())
+
+ // Test the cli command.
+ cmdArgs := []string{
+ "templates",
+ "edit",
+ template.Name,
+ "--max-ttl", "1h",
+ }
+ cmd, root := clitest.New(t, cmdArgs...)
+ clitest.SetupConfig(t, proxyClient, root)
+
+ ctx, _ := testutil.Context(t)
+ err = cmd.ExecuteContext(ctx)
+ require.NoError(t, err)
+
+ require.EqualValues(t, 1, atomic.LoadInt64(&updateTemplateCalled))
+
+ // Assert that the template metadata did not change.
+ updated, err := client.Template(context.Background(), template.ID)
+ require.NoError(t, err)
+ assert.Equal(t, template.Name, updated.Name)
+ assert.Equal(t, template.Description, updated.Description)
+ assert.Equal(t, template.Icon, updated.Icon)
+ assert.Equal(t, template.DisplayName, updated.DisplayName)
+ assert.Equal(t, template.DefaultTTLMillis, updated.DefaultTTLMillis)
+ assert.Equal(t, template.MaxTTLMillis, updated.MaxTTLMillis)
+ })
+ })
}
diff --git a/cli/testdata/coder_templates_edit_--help.golden b/cli/testdata/coder_templates_edit_--help.golden
index f92ba830ad7c6..5bcc48e9a5ae3 100644
--- a/cli/testdata/coder_templates_edit_--help.golden
+++ b/cli/testdata/coder_templates_edit_--help.golden
@@ -7,12 +7,17 @@ Flags:
--allow-user-cancel-workspace-jobs Allow users to cancel in-progress workspace jobs.
(default true)
--default-ttl duration Edit the template default time before shutdown -
- workspaces created from this template to this value.
- --description string Edit the template description
- --display-name string Edit the template display name
+ workspaces created from this template default to
+ this value.
+ --description string Edit the template description.
+ --display-name string Edit the template display name.
-h, --help help for edit
- --icon string Edit the template icon path
- --name string Edit the template name
+ --icon string Edit the template icon path.
+ --max-ttl duration Edit the template maximum time before shutdown -
+ workspaces created from this template must shutdown
+ within the given duration after starting. This is
+ an enterprise-only feature.
+ --name string Edit the template name.
-y, --yes Bypass prompts
Global Flags:
diff --git a/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx b/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx
index 1a274b232b680..cc389bfa4558d 100644
--- a/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx
+++ b/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx
@@ -50,7 +50,10 @@ const defaultInitialValues: CreateTemplateData = {
parameter_values_by_name: undefined,
}
-const getInitialValues = (canSetMaxTTL: boolean, starterTemplate?: TemplateExample) => {
+const getInitialValues = (
+ canSetMaxTTL: boolean,
+ starterTemplate?: TemplateExample,
+) => {
let initialValues = defaultInitialValues
if (!canSetMaxTTL) {
initialValues = {
@@ -97,7 +100,8 @@ export const CreateTemplateForm: FC = ({
const styles = useStyles()
const formFooterStyles = useFormFooterStyles()
const { entitlements } = useDashboard()
- const canSetMaxTTL = entitlements.features["advanced_template_scheduling"].enabled
+ const canSetMaxTTL =
+ entitlements.features["advanced_template_scheduling"].enabled
const form = useFormik({
initialValues: getInitialValues(canSetMaxTTL, starterTemplate),
diff --git a/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx
index 46a0c42ed96e0..7cea8f375aa33 100644
--- a/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx
+++ b/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx
@@ -25,13 +25,19 @@ import { HelpTooltip, HelpTooltipText } from "components/Tooltips/HelpTooltip"
import { makeStyles } from "@material-ui/core/styles"
import { useDashboard } from "components/Dashboard/DashboardProvider"
-+const TTLHelperText = ({ ttl, translationName }: { ttl?: number, translationName: string }) => {
+const TTLHelperText = ({
+ ttl,
+ translationName,
+}: {
+ ttl?: number
+ translationName: string
+}) => {
const { t } = useTranslation("templateSettingsPage")
const count = typeof ttl !== "number" ? 0 : ttl
return (
// no helper text if ttl is negative - error will show once field is considered touched
= 0}>
-+ {t(translationName, { count })}
+ {t(translationName, { count })}
)
}
@@ -88,7 +94,8 @@ export const TemplateSettingsForm: FC = ({
initialTouched,
}) => {
const { entitlements } = useDashboard()
- const canSetMaxTTL = entitlements.features["advanced_template_scheduling"].enabled
+ const canSetMaxTTL =
+ entitlements.features["advanced_template_scheduling"].enabled
const validationSchema = getValidationSchema()
const form: FormikContextType =
@@ -189,7 +196,10 @@ export const TemplateSettingsForm: FC = ({
,
+ ,
)}
disabled={isSubmitting}
fullWidth
@@ -202,13 +212,16 @@ export const TemplateSettingsForm: FC = ({
,
+ "max_ttl_ms",
+ ,
)}
disabled={isSubmitting}
fullWidth
inputProps={{ min: 0, step: 1 }}
- label={t("defaultTtlLabel")}
+ label={t("maxTtlLabel")}
variant="outlined"
type="number"
/>
diff --git a/site/src/pages/TemplateSettingsPage/TemplateSettingsPage.test.tsx b/site/src/pages/TemplateSettingsPage/TemplateSettingsPage.test.tsx
index 94d227e67d4b9..2f100e10aa18a 100644
--- a/site/src/pages/TemplateSettingsPage/TemplateSettingsPage.test.tsx
+++ b/site/src/pages/TemplateSettingsPage/TemplateSettingsPage.test.tsx
@@ -1,13 +1,15 @@
import { screen, waitFor } from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import * as API from "api/api"
-import { UpdateTemplateMeta } from "api/typesGenerated"
+import { Entitlements, UpdateTemplateMeta } from "api/typesGenerated"
import { Language as FooterFormLanguage } from "components/FormFooter/FormFooter"
-import { MockTemplate } from "../../testHelpers/entities"
+import { MockEntitlements, MockTemplate } from "../../testHelpers/entities"
import { renderWithAuth } from "../../testHelpers/renderHelpers"
+import { server } from "../../testHelpers/server"
import { getValidationSchema } from "./TemplateSettingsForm"
import { TemplateSettingsPage } from "./TemplateSettingsPage"
import i18next from "i18next"
+import { rest } from "msw"
const { t } = i18next
@@ -27,7 +29,9 @@ const validFormValues = {
display_name: "A display name",
description: "A description",
icon: "vscode.png",
+ // these are the form values which are actually hours
default_ttl_ms: 1,
+ max_ttl_ms: 2,
allow_user_cancel_workspace_jobs: false,
}
@@ -36,6 +40,7 @@ const fillAndSubmitForm = async ({
display_name,
description,
default_ttl_ms,
+ max_ttl_ms,
icon,
allow_user_cancel_workspace_jobs,
}: Required) => {
@@ -61,9 +66,17 @@ const fillAndSubmitForm = async ({
await userEvent.type(iconField, icon)
const defaultTtlLabel = t("defaultTtlLabel", { ns: "templateSettingsPage" })
- const maxTtlField = await screen.findByLabelText(defaultTtlLabel)
- await userEvent.clear(maxTtlField)
- await userEvent.type(maxTtlField, default_ttl_ms.toString())
+ const defaultTtlField = await screen.findByLabelText(defaultTtlLabel)
+ await userEvent.clear(defaultTtlField)
+ await userEvent.type(defaultTtlField, default_ttl_ms.toString())
+
+ const entitlements = await API.getEntitlements()
+ if (entitlements.features["advanced_template_scheduling"].enabled) {
+ const maxTtlLabel = t("maxTtlLabel", { ns: "templateSettingsPage" })
+ const maxTtlField = await screen.findByLabelText(maxTtlLabel)
+ await userEvent.clear(maxTtlField)
+ await userEvent.type(maxTtlField, max_ttl_ms.toString())
+ }
const allowCancelJobsField = screen.getByRole("checkbox")
// checkbox is checked by default, so it must be clicked to get unchecked
@@ -109,7 +122,60 @@ describe("TemplateSettingsPage", () => {
})
await fillAndSubmitForm(validFormValues)
- expect(screen.getByDisplayValue(1)).toBeInTheDocument() // the default_ttl_ms
+ expect(
+ screen.getByDisplayValue(validFormValues.default_ttl_ms),
+ ).toBeInTheDocument()
+ expect(
+ screen.queryAllByDisplayValue(validFormValues.max_ttl_ms),
+ ).toHaveLength(0)
+ await waitFor(() => expect(API.updateTemplateMeta).toBeCalledTimes(1))
+
+ await waitFor(() =>
+ expect(API.updateTemplateMeta).toBeCalledWith(
+ "test-template",
+ expect.objectContaining({
+ ...validFormValues,
+ // convert from the display value (hours) to ms
+ default_ttl_ms: validFormValues.default_ttl_ms * 3600000,
+ // this value is undefined if not entitled
+ max_ttl_ms: undefined,
+ }),
+ ),
+ )
+ })
+
+ test("max ttl shows if entitled", async () => {
+ const entitlements: Entitlements = {
+ ...MockEntitlements,
+ has_license: true,
+ features: {
+ ...MockEntitlements.features,
+ advanced_template_scheduling: {
+ entitlement: "entitled",
+ enabled: true,
+ },
+ },
+ }
+ server.use(
+ rest.get("/api/v2/entitlements", (req, res, ctx) => {
+ return res(ctx.status(200), ctx.json(entitlements))
+ }),
+ )
+
+ await renderTemplateSettingsPage()
+
+ jest.spyOn(API, "updateTemplateMeta").mockResolvedValueOnce({
+ ...MockTemplate,
+ ...validFormValues,
+ })
+
+ await fillAndSubmitForm(validFormValues)
+ expect(
+ screen.getByDisplayValue(validFormValues.default_ttl_ms),
+ ).toBeInTheDocument()
+ expect(
+ screen.getByDisplayValue(validFormValues.max_ttl_ms),
+ ).toBeInTheDocument()
await waitFor(() => expect(API.updateTemplateMeta).toBeCalledTimes(1))
await waitFor(() =>
@@ -117,7 +183,9 @@ describe("TemplateSettingsPage", () => {
"test-template",
expect.objectContaining({
...validFormValues,
- default_ttl_ms: 3600000, // the default_ttl_ms to ms
+ // convert from the display value (hours) to ms
+ default_ttl_ms: validFormValues.default_ttl_ms * 3600000,
+ max_ttl_ms: validFormValues.max_ttl_ms * 3600000,
}),
),
)
@@ -148,7 +216,7 @@ describe("TemplateSettingsPage", () => {
}
const validate = () => getValidationSchema().validateSync(values)
expect(validate).toThrowError(
- t("ttlMaxError", { ns: "templateSettingsPage" }),
+ t("defaultTTLMaxError", { ns: "templateSettingsPage" }),
)
})
diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts
index 8876bd169a7a2..049ea658d4ef9 100644
--- a/site/src/testHelpers/entities.ts
+++ b/site/src/testHelpers/entities.ts
@@ -266,6 +266,7 @@ export const MockTemplate: TypesGen.Template = {
},
description: "This is a test description.",
default_ttl_ms: 24 * 60 * 60 * 1000,
+ max_ttl_ms: 2 * 24 * 60 * 60 * 1000,
created_by_id: "test-creator-id",
created_by_name: "test_creator",
icon: "/icon/code.svg",
From 4e1d948df80eab6abc37befc45d6980a482281fa Mon Sep 17 00:00:00 2001
From: Dean Sheather
Date: Thu, 23 Feb 2023 16:08:56 +0000
Subject: [PATCH 06/24] fixup! Merge branch 'main' into dean/schedule-max-ttl
---
docs/cli/coder_templates_edit.md | 18 +++++++++++++-----
1 file changed, 13 insertions(+), 5 deletions(-)
diff --git a/docs/cli/coder_templates_edit.md b/docs/cli/coder_templates_edit.md
index e9f59bc9c26bb..60a8b18c6b804 100644
--- a/docs/cli/coder_templates_edit.md
+++ b/docs/cli/coder_templates_edit.md
@@ -22,7 +22,7 @@ Allow users to cancel in-progress workspace jobs.
### --default-ttl
-Edit the template default time before shutdown - workspaces created from this template to this value.
+Edit the template default time before shutdown - workspaces created from this template default to this value.
| | |
| --- | --- |
@@ -30,28 +30,36 @@ Edit the template default time before shutdown - workspaces created from this te
### --description
-Edit the template description
+Edit the template description.
| | |
| --- | --- |
### --display-name
-Edit the template display name
+Edit the template display name.
| | |
| --- | --- |
### --icon
-Edit the template icon path
+Edit the template icon path.
| | |
| --- | --- |
+### --max-ttl
+
+Edit the template maximum time before shutdown - workspaces created from this template must shutdown within the given duration after starting. This is an enterprise-only feature.
+
+| | |
+| --- | --- |
+| Default | 0s
|
+
### --name
-Edit the template name
+Edit the template name.
| | |
| --- | --- |
From 69035a6915fa37ff479e70728c541bb0b2aeb796 Mon Sep 17 00:00:00 2001
From: Dean Sheather
Date: Thu, 23 Feb 2023 17:25:32 +0000
Subject: [PATCH 07/24] feat: block disabling auto off if template has max ttl
---
.../provisionerdserver/provisionerdserver.go | 6 +-
coderd/workspaces.go | 42 ++++++-
coderd/workspaces_test.go | 2 +-
enterprise/coderd/templates_test.go | 114 ++++++++++++++++++
4 files changed, 154 insertions(+), 10 deletions(-)
diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go
index 964a921d19f10..0e7e8c57dab1c 100644
--- a/coderd/provisionerdserver/provisionerdserver.go
+++ b/coderd/provisionerdserver/provisionerdserver.go
@@ -308,7 +308,7 @@ func (server *Server) includeLastVariableValues(ctx context.Context, templateVer
templateVersion, err := server.Database.GetTemplateVersionByID(ctx, templateVersionID)
if err != nil {
- return nil, fmt.Errorf("get template version: %w", err)
+ return nil, xerrors.Errorf("get template version: %w", err)
}
if templateVersion.TemplateID.UUID == uuid.Nil {
@@ -317,7 +317,7 @@ func (server *Server) includeLastVariableValues(ctx context.Context, templateVer
template, err := server.Database.GetTemplateByID(ctx, templateVersion.TemplateID.UUID)
if err != nil {
- return nil, fmt.Errorf("get template: %w", err)
+ return nil, xerrors.Errorf("get template: %w", err)
}
if template.ActiveVersionID == uuid.Nil {
@@ -326,7 +326,7 @@ func (server *Server) includeLastVariableValues(ctx context.Context, templateVer
templateVariables, err := server.Database.GetTemplateVersionVariables(ctx, template.ActiveVersionID)
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
- return nil, fmt.Errorf("get template version variables: %w", err)
+ return nil, xerrors.Errorf("get template version variables: %w", err)
}
for _, templateVariable := range templateVariables {
diff --git a/coderd/workspaces.go b/coderd/workspaces.go
index 929edaad90fb3..9507792672956 100644
--- a/coderd/workspaces.go
+++ b/coderd/workspaces.go
@@ -362,7 +362,16 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
return
}
- dbTTL, err := validWorkspaceTTLMillis(createWorkspace.TTLMillis, template.DefaultTTL)
+ templateSchedule, err := (*api.TemplateScheduleStore.Load()).GetTemplateScheduleOptions(ctx, api.Database, template.ID)
+ if err != nil {
+ httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
+ Message: "Internal error fetching template schedule.",
+ Detail: err.Error(),
+ })
+ return
+ }
+
+ dbTTL, err := validWorkspaceTTLMillis(createWorkspace.TTLMillis, templateSchedule.DefaultTTL, templateSchedule.MaxTTL)
if err != nil {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Invalid Workspace Time to Shutdown.",
@@ -799,9 +808,15 @@ func (api *API) putWorkspaceTTL(rw http.ResponseWriter, r *http.Request) {
var dbTTL sql.NullInt64
err := api.Database.InTx(func(s database.Store) error {
+ templateSchedule, err := (*api.TemplateScheduleStore.Load()).GetTemplateScheduleOptions(ctx, s, workspace.TemplateID)
+ if err != nil {
+ return xerrors.Errorf("get template schedule: %w", err)
+ }
+
+ // don't override 0 ttl with template default here because it indicates
+ // disabled auto-stop
var validityErr error
- // don't override 0 ttl with template default here because it indicates disabled auto-stop
- dbTTL, validityErr = validWorkspaceTTLMillis(req.TTLMillis, 0)
+ dbTTL, validityErr = validWorkspaceTTLMillis(req.TTLMillis, 0, templateSchedule.MaxTTL)
if validityErr != nil {
return codersdk.ValidationError{Field: "ttl_ms", Detail: validityErr.Error()}
}
@@ -1187,14 +1202,25 @@ func convertWorkspaceTTLMillis(i sql.NullInt64) *int64 {
return &millis
}
-func validWorkspaceTTLMillis(millis *int64, def int64) (sql.NullInt64, error) {
+func validWorkspaceTTLMillis(millis *int64, templateDefault, templateMax time.Duration) (sql.NullInt64, error) {
+ if templateDefault == 0 && templateMax != 0 || (templateMax > 0 && templateDefault > templateMax) {
+ templateDefault = templateMax
+ }
+
if ptr.NilOrZero(millis) {
- if def == 0 {
+ if templateDefault == 0 {
+ if templateMax > 0 {
+ return sql.NullInt64{
+ Int64: int64(templateMax),
+ Valid: true,
+ }, nil
+ }
+
return sql.NullInt64{}, nil
}
return sql.NullInt64{
- Int64: def,
+ Int64: int64(templateDefault),
Valid: true,
}, nil
}
@@ -1209,6 +1235,10 @@ func validWorkspaceTTLMillis(millis *int64, def int64) (sql.NullInt64, error) {
return sql.NullInt64{}, errTTLMax
}
+ if templateMax > 0 && truncated > templateMax {
+ return sql.NullInt64{}, xerrors.Errorf("time until shutdown must be less than or equal to the template's maximum TTL %q", templateMax.String())
+ }
+
return sql.NullInt64{
Valid: true,
Int64: int64(truncated),
diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go
index 2b1c7dfe8484c..93c38941c6ba7 100644
--- a/coderd/workspaces_test.go
+++ b/coderd/workspaces_test.go
@@ -331,7 +331,7 @@ func TestPostWorkspacesByOrganization(t *testing.T) {
})
// TTL should be set by the template
require.Equal(t, template.DefaultTTLMillis, templateTTL)
- require.Equal(t, template.DefaultTTLMillis, template.DefaultTTLMillis, workspace.TTLMillis)
+ require.Equal(t, template.DefaultTTLMillis, *workspace.TTLMillis)
})
t.Run("InvalidTTL", func(t *testing.T) {
diff --git a/enterprise/coderd/templates_test.go b/enterprise/coderd/templates_test.go
index c379675edff2d..3157a33ddc4b8 100644
--- a/enterprise/coderd/templates_test.go
+++ b/enterprise/coderd/templates_test.go
@@ -58,6 +58,120 @@ func TestTemplates(t *testing.T) {
require.NoError(t, err)
require.Equal(t, 2*time.Hour, time.Duration(template.MaxTTLMillis)*time.Millisecond)
})
+
+ t.Run("CreateUpdateWorkspaceMaxTTL", func(t *testing.T) {
+ t.Parallel()
+ client := coderdenttest.New(t, &coderdenttest.Options{
+ Options: &coderdtest.Options{
+ IncludeProvisionerDaemon: true,
+ },
+ })
+ user := coderdtest.CreateFirstUser(t, client)
+ _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
+ Features: license.Features{
+ codersdk.FeatureAdvancedTemplateScheduling: 1,
+ },
+ })
+
+ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
+ exp := 24 * time.Hour.Milliseconds()
+ template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
+ ctr.DefaultTTLMillis = &exp
+ ctr.MaxTTLMillis = &exp
+ })
+ coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
+
+ ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
+ defer cancel()
+
+ // No TTL provided should use template default
+ req := codersdk.CreateWorkspaceRequest{
+ TemplateID: template.ID,
+ Name: "testing",
+ }
+ ws, err := client.CreateWorkspace(ctx, template.OrganizationID, codersdk.Me, req)
+ require.NoError(t, err)
+ require.EqualValues(t, exp, *ws.TTLMillis)
+
+ // Editing a workspace to have a higher TTL than the template's max
+ // should error
+ exp = exp + time.Minute.Milliseconds()
+ err = client.UpdateWorkspaceTTL(ctx, ws.ID, codersdk.UpdateWorkspaceTTLRequest{
+ TTLMillis: &exp,
+ })
+ require.Error(t, err)
+ var apiErr *codersdk.Error
+ require.ErrorAs(t, err, &apiErr)
+ require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
+ require.Len(t, apiErr.Validations, 1)
+ require.Equal(t, apiErr.Validations[0].Field, "ttl_ms")
+ require.Contains(t, apiErr.Validations[0].Detail, "time until shutdown must be less than or equal to the template's maximum TTL")
+
+ // Creating workspace with TTL higher than max should error
+ req.Name = "testing2"
+ req.TTLMillis = &exp
+ ws, err = client.CreateWorkspace(ctx, template.OrganizationID, codersdk.Me, req)
+ require.Error(t, err)
+ apiErr = nil
+ require.ErrorAs(t, err, &apiErr)
+ require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
+ require.Len(t, apiErr.Validations, 1)
+ require.Equal(t, apiErr.Validations[0].Field, "ttl_ms")
+ require.Contains(t, apiErr.Validations[0].Detail, "time until shutdown must be less than or equal to the template's maximum TTL")
+ })
+
+ t.Run("BlockDisablingAutoOffWithMaxTTL", func(t *testing.T) {
+ t.Parallel()
+ client := coderdenttest.New(t, &coderdenttest.Options{
+ Options: &coderdtest.Options{
+ IncludeProvisionerDaemon: true,
+ },
+ })
+ user := coderdtest.CreateFirstUser(t, client)
+ _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
+ Features: license.Features{
+ codersdk.FeatureAdvancedTemplateScheduling: 1,
+ },
+ })
+
+ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
+ exp := 24 * time.Hour.Milliseconds()
+ template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
+ ctr.MaxTTLMillis = &exp
+ })
+ coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
+
+ ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
+ defer cancel()
+
+ // No TTL provided should use template default
+ req := codersdk.CreateWorkspaceRequest{
+ TemplateID: template.ID,
+ Name: "testing",
+ }
+ ws, err := client.CreateWorkspace(ctx, template.OrganizationID, codersdk.Me, req)
+ require.NoError(t, err)
+ require.EqualValues(t, exp, *ws.TTLMillis)
+
+ // Editing a workspace to disable the TTL should do nothing
+ err = client.UpdateWorkspaceTTL(ctx, ws.ID, codersdk.UpdateWorkspaceTTLRequest{
+ TTLMillis: nil,
+ })
+ require.NoError(t, err)
+ ws, err = client.Workspace(ctx, ws.ID)
+ require.NoError(t, err)
+ require.EqualValues(t, exp, *ws.TTLMillis)
+
+ // Editing a workspace to have a TTL of 0 should do nothing
+ zero := int64(0)
+ err = client.UpdateWorkspaceTTL(ctx, ws.ID, codersdk.UpdateWorkspaceTTLRequest{
+ TTLMillis: &zero,
+ })
+ require.NoError(t, err)
+ ws, err = client.Workspace(ctx, ws.ID)
+ require.NoError(t, err)
+ require.EqualValues(t, exp, *ws.TTLMillis)
+ })
}
func TestTemplateACL(t *testing.T) {
From 89ccfc8f488739f0fcfa3d225206221fdf579233 Mon Sep 17 00:00:00 2001
From: Dean Sheather
Date: Wed, 1 Mar 2023 14:56:30 +0000
Subject: [PATCH 08/24] fixup! Merge branch 'main' into dean/schedule-max-ttl
---
...line.down.sql => 000102_workspace_build_max_deadline.down.sql} | 0
...deadline.up.sql => 000102_workspace_build_max_deadline.up.sql} | 0
2 files changed, 0 insertions(+), 0 deletions(-)
rename coderd/database/migrations/{000100_workspace_build_max_deadline.down.sql => 000102_workspace_build_max_deadline.down.sql} (100%)
rename coderd/database/migrations/{000100_workspace_build_max_deadline.up.sql => 000102_workspace_build_max_deadline.up.sql} (100%)
diff --git a/coderd/database/migrations/000100_workspace_build_max_deadline.down.sql b/coderd/database/migrations/000102_workspace_build_max_deadline.down.sql
similarity index 100%
rename from coderd/database/migrations/000100_workspace_build_max_deadline.down.sql
rename to coderd/database/migrations/000102_workspace_build_max_deadline.down.sql
diff --git a/coderd/database/migrations/000100_workspace_build_max_deadline.up.sql b/coderd/database/migrations/000102_workspace_build_max_deadline.up.sql
similarity index 100%
rename from coderd/database/migrations/000100_workspace_build_max_deadline.up.sql
rename to coderd/database/migrations/000102_workspace_build_max_deadline.up.sql
From feb7b3cb790e0e621ad6748460cab23ca450ec9c Mon Sep 17 00:00:00 2001
From: Dean Sheather
Date: Wed, 1 Mar 2023 16:30:06 +0000
Subject: [PATCH 09/24] feat: apply template max TTL to workspace TTL on update
---
coderd/database/dbauthz/querier.go | 7 +
coderd/database/dbfake/databasefake.go | 21 ++
coderd/database/querier.go | 1 +
coderd/database/queries.sql.go | 25 ++
coderd/database/queries/workspaces.sql | 14 +
coderd/database/sqlc.yaml | 1 +
.../provisionerdserver_test.go | 299 +++++++++++++-----
enterprise/coderd/provisionerdaemons.go | 27 +-
enterprise/coderd/templates_test.go | 33 +-
9 files changed, 352 insertions(+), 76 deletions(-)
diff --git a/coderd/database/dbauthz/querier.go b/coderd/database/dbauthz/querier.go
index 2005faec5d527..b9b2bd4669317 100644
--- a/coderd/database/dbauthz/querier.go
+++ b/coderd/database/dbauthz/querier.go
@@ -1499,6 +1499,13 @@ func (q *querier) UpdateWorkspaceLastUsedAt(ctx context.Context, arg database.Up
return update(q.log, q.auth, fetch, q.db.UpdateWorkspaceLastUsedAt)(ctx, arg)
}
+func (q *querier) UpdateWorkspaceTTLToBeWithinTemplateMax(ctx context.Context, arg database.UpdateWorkspaceTTLToBeWithinTemplateMaxParams) error {
+ fetch := func(ctx context.Context, arg database.UpdateWorkspaceTTLToBeWithinTemplateMaxParams) (database.Template, error) {
+ return q.db.GetTemplateByID(ctx, arg.TemplateID)
+ }
+ return fetchAndExec(q.log, q.auth, rbac.ActionUpdate, fetch, q.db.UpdateWorkspaceTTLToBeWithinTemplateMax)(ctx, arg)
+}
+
func (q *querier) UpdateWorkspaceTTL(ctx context.Context, arg database.UpdateWorkspaceTTLParams) error {
fetch := func(ctx context.Context, arg database.UpdateWorkspaceTTLParams) (database.Workspace, error) {
return q.db.GetWorkspaceByID(ctx, arg.ID)
diff --git a/coderd/database/dbfake/databasefake.go b/coderd/database/dbfake/databasefake.go
index 8da322627dfab..355a20092c062 100644
--- a/coderd/database/dbfake/databasefake.go
+++ b/coderd/database/dbfake/databasefake.go
@@ -3515,6 +3515,26 @@ func (q *fakeQuerier) UpdateWorkspaceLastUsedAt(_ context.Context, arg database.
return sql.ErrNoRows
}
+func (q *fakeQuerier) UpdateWorkspaceTTLToBeWithinTemplateMax(_ context.Context, arg database.UpdateWorkspaceTTLToBeWithinTemplateMaxParams) error {
+ if err := validateDatabaseType(arg); err != nil {
+ return err
+ }
+
+ q.mutex.Lock()
+ defer q.mutex.Unlock()
+
+ for index, workspace := range q.workspaces {
+ if workspace.TemplateID != arg.TemplateID || !workspace.Ttl.Valid || workspace.Ttl.Int64 < arg.TemplateMaxTTL {
+ continue
+ }
+
+ workspace.Ttl = sql.NullInt64{Int64: arg.TemplateMaxTTL, Valid: true}
+ q.workspaces[index] = workspace
+ }
+
+ return nil
+}
+
func (q *fakeQuerier) UpdateWorkspaceBuildByID(_ context.Context, arg database.UpdateWorkspaceBuildByIDParams) (database.WorkspaceBuild, error) {
if err := validateDatabaseType(arg); err != nil {
return database.WorkspaceBuild{}, err
@@ -3530,6 +3550,7 @@ func (q *fakeQuerier) UpdateWorkspaceBuildByID(_ context.Context, arg database.U
workspaceBuild.UpdatedAt = arg.UpdatedAt
workspaceBuild.ProvisionerState = arg.ProvisionerState
workspaceBuild.Deadline = arg.Deadline
+ workspaceBuild.MaxDeadline = arg.MaxDeadline
q.workspaceBuilds[index] = workspaceBuild
return workspaceBuild, nil
}
diff --git a/coderd/database/querier.go b/coderd/database/querier.go
index f894b4e23f872..18b3c329642e9 100644
--- a/coderd/database/querier.go
+++ b/coderd/database/querier.go
@@ -211,6 +211,7 @@ type sqlcQuerier interface {
UpdateWorkspaceDeletedByID(ctx context.Context, arg UpdateWorkspaceDeletedByIDParams) error
UpdateWorkspaceLastUsedAt(ctx context.Context, arg UpdateWorkspaceLastUsedAtParams) error
UpdateWorkspaceTTL(ctx context.Context, arg UpdateWorkspaceTTLParams) error
+ UpdateWorkspaceTTLToBeWithinTemplateMax(ctx context.Context, arg UpdateWorkspaceTTLToBeWithinTemplateMaxParams) error
}
var _ sqlcQuerier = (*sqlQuerier)(nil)
diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go
index ed88196786950..e606845d90055 100644
--- a/coderd/database/queries.sql.go
+++ b/coderd/database/queries.sql.go
@@ -7281,3 +7281,28 @@ func (q *sqlQuerier) UpdateWorkspaceTTL(ctx context.Context, arg UpdateWorkspace
_, err := q.db.ExecContext(ctx, updateWorkspaceTTL, arg.ID, arg.Ttl)
return err
}
+
+const updateWorkspaceTTLToBeWithinTemplateMax = `-- name: UpdateWorkspaceTTLToBeWithinTemplateMax :exec
+UPDATE
+ workspaces
+SET
+ ttl = LEAST(ttl, $1::bigint)
+WHERE
+ template_id = $2
+ -- LEAST() does not pick NULL, so filter it out as we don't want to set a
+ -- TTL on the workspace if it's unset.
+ --
+ -- During build time, the template max TTL will still be used if the
+ -- workspace TTL is NULL.
+ AND ttl IS NOT NULL
+`
+
+type UpdateWorkspaceTTLToBeWithinTemplateMaxParams struct {
+ TemplateMaxTTL int64 `db:"template_max_ttl" json:"template_max_ttl"`
+ TemplateID uuid.UUID `db:"template_id" json:"template_id"`
+}
+
+func (q *sqlQuerier) UpdateWorkspaceTTLToBeWithinTemplateMax(ctx context.Context, arg UpdateWorkspaceTTLToBeWithinTemplateMaxParams) error {
+ _, err := q.db.ExecContext(ctx, updateWorkspaceTTLToBeWithinTemplateMax, arg.TemplateMaxTTL, arg.TemplateID)
+ return err
+}
diff --git a/coderd/database/queries/workspaces.sql b/coderd/database/queries/workspaces.sql
index def4436bed94c..3beedc6932089 100644
--- a/coderd/database/queries/workspaces.sql
+++ b/coderd/database/queries/workspaces.sql
@@ -316,3 +316,17 @@ SET
last_used_at = $2
WHERE
id = $1;
+
+-- name: UpdateWorkspaceTTLToBeWithinTemplateMax :exec
+UPDATE
+ workspaces
+SET
+ ttl = LEAST(ttl, @template_max_ttl::bigint)
+WHERE
+ template_id = @template_id
+ -- LEAST() does not pick NULL, so filter it out as we don't want to set a
+ -- TTL on the workspace if it's unset.
+ --
+ -- During build time, the template max TTL will still be used if the
+ -- workspace TTL is NULL.
+ AND ttl IS NOT NULL;
diff --git a/coderd/database/sqlc.yaml b/coderd/database/sqlc.yaml
index 5cf4037ffc82a..fbe058988e2c7 100644
--- a/coderd/database/sqlc.yaml
+++ b/coderd/database/sqlc.yaml
@@ -43,6 +43,7 @@ overrides:
troubleshooting_url: TroubleshootingURL
default_ttl: DefaultTTL
max_ttl: MaxTTL
+ template_max_ttl: TemplateMaxTTL
motd_file: MOTDFile
uuid: UUID
diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go
index a6e6614fe295f..8bc367ae1d3fa 100644
--- a/coderd/provisionerdserver/provisionerdserver_test.go
+++ b/coderd/provisionerdserver/provisionerdserver_test.go
@@ -792,89 +792,226 @@ func TestCompleteJob(t *testing.T) {
require.NoError(t, err)
require.False(t, job.Error.Valid)
})
+
t.Run("WorkspaceBuild", func(t *testing.T) {
t.Parallel()
- srv := setup(t, false)
- user := dbgen.User(t, srv.Database, database.User{})
- template := dbgen.Template(t, srv.Database, database.Template{
- Name: "template",
- Provisioner: database.ProvisionerTypeEcho,
- })
- file := dbgen.File(t, srv.Database, database.File{CreatedBy: user.ID})
- workspace, err := srv.Database.InsertWorkspace(ctx, database.InsertWorkspaceParams{
- ID: uuid.New(),
- TemplateID: template.ID,
- })
- version := dbgen.TemplateVersion(t, srv.Database, database.TemplateVersion{
- TemplateID: uuid.NullUUID{
- UUID: template.ID,
- Valid: true,
+ cases := []struct {
+ name string
+ templateDefaultTTL time.Duration
+ templateMaxTTL time.Duration
+ workspaceTTL time.Duration
+ transition database.WorkspaceTransition
+ // The TTL is actually a deadline time on the workspace_build row,
+ // so during the test this will be compared to be within 15 seconds
+ // of the expected value.
+ expectedTTL time.Duration
+ expectedMaxTTL time.Duration
+ }{
+ {
+ name: "OK",
+ templateDefaultTTL: 0,
+ templateMaxTTL: 0,
+ workspaceTTL: 0,
+ transition: database.WorkspaceTransitionStart,
+ expectedTTL: 0,
+ expectedMaxTTL: 0,
},
- JobID: uuid.New(),
- })
- require.NoError(t, err)
- build, err := srv.Database.InsertWorkspaceBuild(ctx, database.InsertWorkspaceBuildParams{
- ID: uuid.New(),
- WorkspaceID: workspace.ID,
- TemplateVersionID: version.ID,
- Transition: database.WorkspaceTransitionDelete,
- Reason: database.BuildReasonInitiator,
- })
- require.NoError(t, err)
- job, err := srv.Database.InsertProvisionerJob(ctx, database.InsertProvisionerJobParams{
- ID: uuid.New(),
- FileID: file.ID,
- Provisioner: database.ProvisionerTypeEcho,
- Type: database.ProvisionerJobTypeWorkspaceBuild,
- StorageMethod: database.ProvisionerStorageMethodFile,
- Input: must(json.Marshal(provisionerdserver.WorkspaceProvisionJob{
- WorkspaceBuildID: build.ID,
- })),
- })
- require.NoError(t, err)
- _, err = srv.Database.AcquireProvisionerJob(ctx, database.AcquireProvisionerJobParams{
- WorkerID: uuid.NullUUID{
- UUID: srv.ID,
- Valid: true,
+ {
+ name: "Delete",
+ templateDefaultTTL: 0,
+ templateMaxTTL: 0,
+ workspaceTTL: 0,
+ transition: database.WorkspaceTransitionDelete,
+ expectedTTL: 0,
+ expectedMaxTTL: 0,
},
- Types: []database.ProvisionerType{database.ProvisionerTypeEcho},
- })
- require.NoError(t, err)
+ {
+ name: "WorkspaceTTL",
+ templateDefaultTTL: 0,
+ templateMaxTTL: 0,
+ workspaceTTL: time.Hour,
+ transition: database.WorkspaceTransitionStart,
+ expectedTTL: time.Hour,
+ expectedMaxTTL: 0,
+ },
+ {
+ name: "TemplateDefaultTTL",
+ templateDefaultTTL: time.Hour,
+ templateMaxTTL: 0,
+ workspaceTTL: 0,
+ transition: database.WorkspaceTransitionStart,
+ expectedTTL: time.Hour,
+ expectedMaxTTL: 0,
+ },
+ {
+ name: "WorkspaceTTLOverridesTemplateDefaultTTL",
+ templateDefaultTTL: 2 * time.Hour,
+ templateMaxTTL: 0,
+ workspaceTTL: time.Hour,
+ transition: database.WorkspaceTransitionStart,
+ expectedTTL: time.Hour,
+ expectedMaxTTL: 0,
+ },
+ {
+ name: "TemplateMaxTTL",
+ templateDefaultTTL: 0,
+ templateMaxTTL: time.Hour,
+ workspaceTTL: 0,
+ transition: database.WorkspaceTransitionStart,
+ expectedTTL: time.Hour,
+ expectedMaxTTL: time.Hour,
+ },
+ {
+ name: "TemplateMaxTTLOverridesWorkspaceTTL",
+ templateDefaultTTL: 0,
+ templateMaxTTL: 2 * time.Hour,
+ workspaceTTL: 3 * time.Hour,
+ transition: database.WorkspaceTransitionStart,
+ expectedTTL: 2 * time.Hour,
+ expectedMaxTTL: 2 * time.Hour,
+ },
+ {
+ name: "TemplateMaxTTLOverridesTemplateDefaultTTL",
+ templateDefaultTTL: 3 * time.Hour,
+ templateMaxTTL: 2 * time.Hour,
+ workspaceTTL: 0,
+ transition: database.WorkspaceTransitionStart,
+ expectedTTL: 2 * time.Hour,
+ expectedMaxTTL: 2 * time.Hour,
+ },
+ }
- publishedWorkspace := make(chan struct{})
- closeWorkspaceSubscribe, err := srv.Pubsub.Subscribe(codersdk.WorkspaceNotifyChannel(build.WorkspaceID), func(_ context.Context, _ []byte) {
- close(publishedWorkspace)
- })
- require.NoError(t, err)
- defer closeWorkspaceSubscribe()
- publishedLogs := make(chan struct{})
- closeLogsSubscribe, err := srv.Pubsub.Subscribe(provisionerdserver.ProvisionerJobLogsNotifyChannel(job.ID), func(_ context.Context, _ []byte) {
- close(publishedLogs)
- })
- require.NoError(t, err)
- defer closeLogsSubscribe()
+ for _, c := range cases {
+ c := c
- _, err = srv.CompleteJob(ctx, &proto.CompletedJob{
- JobId: job.ID.String(),
- Type: &proto.CompletedJob_WorkspaceBuild_{
- WorkspaceBuild: &proto.CompletedJob_WorkspaceBuild{
- State: []byte{},
- Resources: []*sdkproto.Resource{{
- Name: "example",
- Type: "aws_instance",
- }},
- },
- },
- })
- require.NoError(t, err)
+ t.Run(c.name, func(t *testing.T) {
+ t.Parallel()
- <-publishedWorkspace
- <-publishedLogs
+ srv := setup(t, false)
- workspace, err = srv.Database.GetWorkspaceByID(ctx, workspace.ID)
- require.NoError(t, err)
- require.True(t, workspace.Deleted)
+ var store provisionerdserver.TemplateScheduleStore = mockTemplateScheduleStore{
+ GetFn: func(_ context.Context, _ database.Store, _ uuid.UUID) (provisionerdserver.TemplateScheduleOptions, error) {
+ return provisionerdserver.TemplateScheduleOptions{
+ UserSchedulingEnabled: true,
+ DefaultTTL: c.templateDefaultTTL,
+ MaxTTL: c.templateMaxTTL,
+ }, nil
+ },
+ }
+ srv.TemplateScheduleStore.Store(&store)
+
+ user := dbgen.User(t, srv.Database, database.User{})
+ template := dbgen.Template(t, srv.Database, database.Template{
+ Name: "template",
+ Provisioner: database.ProvisionerTypeEcho,
+ })
+ template, err := srv.Database.UpdateTemplateScheduleByID(ctx, database.UpdateTemplateScheduleByIDParams{
+ ID: template.ID,
+ UpdatedAt: database.Now(),
+ DefaultTTL: int64(c.templateDefaultTTL),
+ MaxTTL: int64(c.templateMaxTTL),
+ })
+ require.NoError(t, err)
+ file := dbgen.File(t, srv.Database, database.File{CreatedBy: user.ID})
+ workspaceTTL := sql.NullInt64{}
+ if c.workspaceTTL != 0 {
+ workspaceTTL = sql.NullInt64{
+ Int64: int64(c.workspaceTTL),
+ Valid: true,
+ }
+ }
+ workspace, err := srv.Database.InsertWorkspace(ctx, database.InsertWorkspaceParams{
+ ID: uuid.New(),
+ TemplateID: template.ID,
+ Ttl: workspaceTTL,
+ })
+ version := dbgen.TemplateVersion(t, srv.Database, database.TemplateVersion{
+ TemplateID: uuid.NullUUID{
+ UUID: template.ID,
+ Valid: true,
+ },
+ JobID: uuid.New(),
+ })
+ require.NoError(t, err)
+ build, err := srv.Database.InsertWorkspaceBuild(ctx, database.InsertWorkspaceBuildParams{
+ ID: uuid.New(),
+ WorkspaceID: workspace.ID,
+ TemplateVersionID: version.ID,
+ Transition: c.transition,
+ Reason: database.BuildReasonInitiator,
+ })
+ require.NoError(t, err)
+ job, err := srv.Database.InsertProvisionerJob(ctx, database.InsertProvisionerJobParams{
+ ID: uuid.New(),
+ FileID: file.ID,
+ Provisioner: database.ProvisionerTypeEcho,
+ Type: database.ProvisionerJobTypeWorkspaceBuild,
+ StorageMethod: database.ProvisionerStorageMethodFile,
+ Input: must(json.Marshal(provisionerdserver.WorkspaceProvisionJob{
+ WorkspaceBuildID: build.ID,
+ })),
+ })
+ require.NoError(t, err)
+ _, err = srv.Database.AcquireProvisionerJob(ctx, database.AcquireProvisionerJobParams{
+ WorkerID: uuid.NullUUID{
+ UUID: srv.ID,
+ Valid: true,
+ },
+ Types: []database.ProvisionerType{database.ProvisionerTypeEcho},
+ })
+ require.NoError(t, err)
+
+ publishedWorkspace := make(chan struct{})
+ closeWorkspaceSubscribe, err := srv.Pubsub.Subscribe(codersdk.WorkspaceNotifyChannel(build.WorkspaceID), func(_ context.Context, _ []byte) {
+ close(publishedWorkspace)
+ })
+ require.NoError(t, err)
+ defer closeWorkspaceSubscribe()
+ publishedLogs := make(chan struct{})
+ closeLogsSubscribe, err := srv.Pubsub.Subscribe(provisionerdserver.ProvisionerJobLogsNotifyChannel(job.ID), func(_ context.Context, _ []byte) {
+ close(publishedLogs)
+ })
+ require.NoError(t, err)
+ defer closeLogsSubscribe()
+
+ _, err = srv.CompleteJob(ctx, &proto.CompletedJob{
+ JobId: job.ID.String(),
+ Type: &proto.CompletedJob_WorkspaceBuild_{
+ WorkspaceBuild: &proto.CompletedJob_WorkspaceBuild{
+ State: []byte{},
+ Resources: []*sdkproto.Resource{{
+ Name: "example",
+ Type: "aws_instance",
+ }},
+ },
+ },
+ })
+ require.NoError(t, err)
+
+ <-publishedWorkspace
+ <-publishedLogs
+
+ workspace, err = srv.Database.GetWorkspaceByID(ctx, workspace.ID)
+ require.NoError(t, err)
+ require.Equal(t, c.transition == database.WorkspaceTransitionDelete, workspace.Deleted)
+
+ workspaceBuild, err := srv.Database.GetWorkspaceBuildByID(ctx, build.ID)
+ require.NoError(t, err)
+
+ if c.expectedTTL == 0 {
+ require.True(t, workspaceBuild.Deadline.IsZero())
+ } else {
+ require.WithinDuration(t, time.Now().Add(c.expectedTTL), workspaceBuild.Deadline, 15*time.Second, "deadline does not match expected")
+ }
+ if c.expectedMaxTTL == 0 {
+ require.True(t, workspaceBuild.MaxDeadline.IsZero())
+ } else {
+ require.WithinDuration(t, time.Now().Add(c.expectedMaxTTL), workspaceBuild.MaxDeadline, 15*time.Second, "max deadline does not match expected")
+ require.GreaterOrEqual(t, workspaceBuild.MaxDeadline.Unix(), workspaceBuild.Deadline.Unix(), "max deadline is smaller than deadline")
+ }
+ })
+ }
})
t.Run("TemplateDryRun", func(t *testing.T) {
@@ -1028,3 +1165,17 @@ func must[T any](value T, err error) T {
}
return value
}
+
+type mockTemplateScheduleStore struct {
+ GetFn func(ctx context.Context, db database.Store, id uuid.UUID) (provisionerdserver.TemplateScheduleOptions, error)
+}
+
+var _ provisionerdserver.TemplateScheduleStore = mockTemplateScheduleStore{}
+
+func (mockTemplateScheduleStore) SetTemplateScheduleOptions(ctx context.Context, db database.Store, template database.Template, opts provisionerdserver.TemplateScheduleOptions) (database.Template, error) {
+ return provisionerdserver.NewAGPLTemplateScheduleStore().SetTemplateScheduleOptions(ctx, db, template, opts)
+}
+
+func (m mockTemplateScheduleStore) GetTemplateScheduleOptions(ctx context.Context, db database.Store, id uuid.UUID) (provisionerdserver.TemplateScheduleOptions, error) {
+ return m.GetFn(ctx, db, id)
+}
diff --git a/enterprise/coderd/provisionerdaemons.go b/enterprise/coderd/provisionerdaemons.go
index 9cfaa2c9e3f3a..caddf10292e8d 100644
--- a/enterprise/coderd/provisionerdaemons.go
+++ b/enterprise/coderd/provisionerdaemons.go
@@ -323,10 +323,35 @@ func (*enterpriseTemplateScheduleStore) GetTemplateScheduleOptions(ctx context.C
}
func (*enterpriseTemplateScheduleStore) SetTemplateScheduleOptions(ctx context.Context, db database.Store, tpl database.Template, opts provisionerdserver.TemplateScheduleOptions) (database.Template, error) {
- return db.UpdateTemplateScheduleByID(ctx, database.UpdateTemplateScheduleByIDParams{
+ template, err := db.UpdateTemplateScheduleByID(ctx, database.UpdateTemplateScheduleByIDParams{
ID: tpl.ID,
UpdatedAt: database.Now(),
DefaultTTL: int64(opts.DefaultTTL),
MaxTTL: int64(opts.MaxTTL),
})
+ if err != nil {
+ return database.Template{}, xerrors.Errorf("update template schedule: %w", err)
+ }
+
+ // Update all workspaces using the template to set the user defined schedule
+ // to be within the new bounds. This essentially does the following for each
+ // workspace using the template.
+ // if (template.ttl != NULL) {
+ // workspace.ttl = min(workspace.ttl, template.ttl)
+ // }
+ //
+ // NOTE: this does not apply to currently running workspaces as their
+ // schedule information is committed to the workspace_build during start.
+ // This limitation is displayed to the user while editing the template.
+ if opts.MaxTTL > 0 {
+ err = db.UpdateWorkspaceTTLToBeWithinTemplateMax(ctx, database.UpdateWorkspaceTTLToBeWithinTemplateMaxParams{
+ TemplateID: template.ID,
+ TemplateMaxTTL: int64(opts.MaxTTL),
+ })
+ if err != nil {
+ return database.Template{}, xerrors.Errorf("update TTL of all workspaces on template to be within new template max TTL: %w", err)
+ }
+ }
+
+ return template, nil
}
diff --git a/enterprise/coderd/templates_test.go b/enterprise/coderd/templates_test.go
index 3157a33ddc4b8..b2e240ab2ae7d 100644
--- a/enterprise/coderd/templates_test.go
+++ b/enterprise/coderd/templates_test.go
@@ -28,7 +28,11 @@ func TestTemplates(t *testing.T) {
t.Run("SetMaxTTL", func(t *testing.T) {
t.Parallel()
- client := coderdenttest.New(t, nil)
+ client := coderdenttest.New(t, &coderdenttest.Options{
+ Options: &coderdtest.Options{
+ IncludeProvisionerDaemon: true,
+ },
+ })
user := coderdtest.CreateFirstUser(t, client)
_ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
Features: license.Features{
@@ -37,9 +41,23 @@ func TestTemplates(t *testing.T) {
})
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
+ coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
require.EqualValues(t, 0, template.MaxTTLMillis)
+ // Create some workspaces to test propagation to user-defined TTLs.
+ workspace1 := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
+ ttl := (24 * time.Hour).Milliseconds()
+ cwr.TTLMillis = &ttl
+ })
+ workspace2TTL := (1 * time.Hour).Milliseconds()
+ workspace2 := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
+ cwr.TTLMillis = &workspace2TTL
+ })
+ workspace3 := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
+ cwr.TTLMillis = nil
+ })
+
ctx, _ := testutil.Context(t)
updated, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
@@ -57,6 +75,19 @@ func TestTemplates(t *testing.T) {
template, err = client.Template(ctx, template.ID)
require.NoError(t, err)
require.Equal(t, 2*time.Hour, time.Duration(template.MaxTTLMillis)*time.Millisecond)
+
+ // Verify that only the first workspace has been updated.
+ workspace1, err = client.Workspace(ctx, workspace1.ID)
+ require.NoError(t, err)
+ require.Equal(t, &template.MaxTTLMillis, workspace1.TTLMillis)
+
+ workspace2, err = client.Workspace(ctx, workspace2.ID)
+ require.NoError(t, err)
+ require.Equal(t, &workspace2TTL, workspace2.TTLMillis)
+
+ workspace3, err = client.Workspace(ctx, workspace3.ID)
+ require.NoError(t, err)
+ require.Nil(t, workspace3.TTLMillis)
})
t.Run("CreateUpdateWorkspaceMaxTTL", func(t *testing.T) {
From d54d7983e4c2439c48eaa7117f9ee06cdfe19ab7 Mon Sep 17 00:00:00 2001
From: Dean Sheather
Date: Wed, 1 Mar 2023 17:11:51 +0000
Subject: [PATCH 10/24] chore: fix tests and differences between sql and memory
db
---
cli/schedule_test.go | 7 +++++--
coderd/provisionerdserver/provisionerdserver.go | 3 ---
.../provisionerdserver_test.go | 4 ++--
coderd/templates.go | 16 +++++++---------
enterprise/coderd/templates_test.go | 10 ++++++----
5 files changed, 20 insertions(+), 20 deletions(-)
diff --git a/cli/schedule_test.go b/cli/schedule_test.go
index 8fc7c9b50b6c8..cd30de7d7f551 100644
--- a/cli/schedule_test.go
+++ b/cli/schedule_test.go
@@ -308,13 +308,16 @@ func TestScheduleOverride(t *testing.T) {
user = coderdtest.CreateFirstUser(t, client)
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
- project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
- workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
+ template = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
+ workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
cwr.TTLMillis = nil
})
cmdArgs = []string{"schedule", "override-stop", workspace.Name, "1h"}
stdoutBuf = &bytes.Buffer{}
)
+ require.Zero(t, template.DefaultTTLMillis)
+ require.Zero(t, template.MaxTTLMillis)
+
// Unset the workspace TTL
err = client.UpdateWorkspaceTTL(ctx, workspace.ID, codersdk.UpdateWorkspaceTTLRequest{TTLMillis: nil})
require.NoError(t, err)
diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go
index dc98ba5e5127e..b7538abd0e5df 100644
--- a/coderd/provisionerdserver/provisionerdserver.go
+++ b/coderd/provisionerdserver/provisionerdserver.go
@@ -917,9 +917,6 @@ func (server *Server) CompleteJob(ctx context.Context, completed *proto.Complete
// The user is not permitted to set their own TTL.
deadline = time.Time{}
}
- if deadline.IsZero() && templateSchedule.DefaultTTL > 0 {
- deadline = now.Add(templateSchedule.DefaultTTL)
- }
if templateSchedule.MaxTTL > 0 {
maxDeadline = now.Add(templateSchedule.MaxTTL)
diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go
index 8bc367ae1d3fa..518be8e25c55c 100644
--- a/coderd/provisionerdserver/provisionerdserver_test.go
+++ b/coderd/provisionerdserver/provisionerdserver_test.go
@@ -836,12 +836,12 @@ func TestCompleteJob(t *testing.T) {
expectedMaxTTL: 0,
},
{
- name: "TemplateDefaultTTL",
+ name: "TemplateDefaultTTLIgnored",
templateDefaultTTL: time.Hour,
templateMaxTTL: 0,
workspaceTTL: 0,
transition: database.WorkspaceTransitionStart,
- expectedTTL: time.Hour,
+ expectedTTL: 0,
expectedMaxTTL: 0,
},
{
diff --git a/coderd/templates.go b/coderd/templates.go
index 0066941962f2f..0d0d0b5e9ed83 100644
--- a/coderd/templates.go
+++ b/coderd/templates.go
@@ -273,15 +273,13 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
return xerrors.Errorf("insert template: %s", err)
}
- if defaultTTL != 0 || maxTTL != 0 {
- dbTemplate, err = (*api.TemplateScheduleStore.Load()).SetTemplateScheduleOptions(ctx, tx, dbTemplate, provisionerdserver.TemplateScheduleOptions{
- UserSchedulingEnabled: true,
- DefaultTTL: defaultTTL,
- MaxTTL: maxTTL,
- })
- if err != nil {
- return xerrors.Errorf("set template schedule options: %s", err)
- }
+ dbTemplate, err = (*api.TemplateScheduleStore.Load()).SetTemplateScheduleOptions(ctx, tx, dbTemplate, provisionerdserver.TemplateScheduleOptions{
+ UserSchedulingEnabled: true,
+ DefaultTTL: defaultTTL,
+ MaxTTL: maxTTL,
+ })
+ if err != nil {
+ return xerrors.Errorf("set template schedule options: %s", err)
}
templateAudit.New = dbTemplate
diff --git a/enterprise/coderd/templates_test.go b/enterprise/coderd/templates_test.go
index b2e240ab2ae7d..503637e7e441a 100644
--- a/enterprise/coderd/templates_test.go
+++ b/enterprise/coderd/templates_test.go
@@ -54,11 +54,13 @@ func TestTemplates(t *testing.T) {
workspace2 := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
cwr.TTLMillis = &workspace2TTL
})
- workspace3 := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
- cwr.TTLMillis = nil
- })
-
+ workspace3 := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
+ // To unset TTL you have to update, as setting a nil TTL on create
+ // copies the template default TTL.
ctx, _ := testutil.Context(t)
+ err := client.UpdateWorkspaceTTL(ctx, workspace3.ID, codersdk.UpdateWorkspaceTTLRequest{
+ TTLMillis: nil,
+ })
updated, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
Name: template.Name,
From 6caeb003cccb7940eb2040f8773c3053b732a2a5 Mon Sep 17 00:00:00 2001
From: Bruno Quaresma
Date: Wed, 1 Mar 2023 17:25:48 +0000
Subject: [PATCH 11/24] Few refactorings for ttl fields
---
site/src/i18n/en/common.json | 4 +-
site/src/i18n/en/createTemplatePage.json | 19 ++-
site/src/i18n/en/templateSettingsPage.json | 16 +--
.../CreateTemplatePage/CreateTemplateForm.tsx | 115 ++++++++++++++----
.../TemplateSettingsForm.tsx | 63 ++++++----
.../TemplateSettingsPage.tsx | 5 +
.../TemplateSettingsPageView.stories.tsx | 20 +--
.../TemplateSettingsPageView.tsx | 3 +
8 files changed, 177 insertions(+), 68 deletions(-)
diff --git a/site/src/i18n/en/common.json b/site/src/i18n/en/common.json
index f1b4116b244b5..61d268b5ba8cd 100644
--- a/site/src/i18n/en/common.json
+++ b/site/src/i18n/en/common.json
@@ -39,5 +39,7 @@
"updateCheck": {
"message": "Coder {{version}} is now available. View the <4>release notes4> and <7>upgrade instructions7> for more information.",
"error": "Coder update check failed."
- }
+ },
+ "licenseFieldTextHelper": "You need an enterprise license to use it.",
+ "learnMore": "Learn more"
}
diff --git a/site/src/i18n/en/createTemplatePage.json b/site/src/i18n/en/createTemplatePage.json
index 996adb276e473..15696c4157598 100644
--- a/site/src/i18n/en/createTemplatePage.json
+++ b/site/src/i18n/en/createTemplatePage.json
@@ -26,13 +26,17 @@
"displayName": "Display name",
"description": "Description",
"icon": "Icon",
- "autoStop": "Auto-stop default",
- "maxTTL": "Maximum lifetime of workspaces",
+ "autoStop": "Default auto-stop",
+ "maxTTL": "Max. Lifetime",
"allowUsersToCancel": "Allow users to cancel in-progress workspace jobs"
},
"helperText": {
- "autoStop": "Time in hours",
- "maxTTL": "Time in hours",
+ "defaultTTLHelperText_zero": "Workspaces will run until stopped manually.",
+ "defaultTTLHelperText_one": "Workspaces will default to stopping after {{count}} hour.",
+ "defaultTTLHelperText_other": "Workspaces will default to stopping after {{count}} hours.",
+ "maxTTLHelperText_zero": "Workspaces may run indefinitely.",
+ "maxTTLHelperText_one": "Workspaces must stop within 1 hour of starting.",
+ "maxTTLHelperText_other": "Workspaces must stop within {{count}} hours of starting.",
"allowUsersToCancel": "If checked, users may be able to corrupt their workspace."
},
"upload": {
@@ -41,6 +45,13 @@
},
"tooltip": {
"allowUsersToCancel": "Depending on your template, canceling builds may leave workspaces in an unhealthy state. This option isn't recommended for most use cases."
+ },
+ "error": {
+ "descriptionMax": "Please enter a description that is less than or equal to 128 characters.",
+ "defaultTTLMax": "Please enter a limit that is less than or equal to 168 hours (7 days).",
+ "defaultTTLMin": "Default time until auto-stop must not be less than 0.",
+ "maxTTLMax": "Please enter a limit that is less than or equal to 168 hours (7 days).",
+ "maxTTLMin": "Maximum time until auto-stop must not be less than 0."
}
}
}
diff --git a/site/src/i18n/en/templateSettingsPage.json b/site/src/i18n/en/templateSettingsPage.json
index edcaf469a6726..89337d99b5664 100644
--- a/site/src/i18n/en/templateSettingsPage.json
+++ b/site/src/i18n/en/templateSettingsPage.json
@@ -4,21 +4,21 @@
"displayNameLabel": "Display name",
"descriptionLabel": "Description",
"descriptionMaxError": "Please enter a description that is less than or equal to 128 characters.",
- "defaultTtlLabel": "Auto-stop default",
- "maxTtlLabel": "Maximum lifetime for started workspaces",
+ "defaultTtlLabel": "Default auto-stop",
+ "maxTtlLabel": "Max. Lifetime",
"iconLabel": "Icon",
"formAriaLabel": "Template settings form",
"selectEmoji": "Select emoji",
"defaultTTLMaxError": "Please enter a limit that is less than or equal to 168 hours (7 days).",
"defaultTTLMinError": "Default time until auto-stop must not be less than 0.",
- "defaultTTLHelperText_zero": "Workspaces created from this template will run until stopped manually.",
- "defaultTTLHelperText_one": "Workspaces created from this template will default to stopping after {{count}} hour.",
- "defaultTTLHelperText_other": "Workspaces created from this template will default to stopping after {{count}} hours.",
+ "defaultTTLHelperText_zero": "Workspaces will run until stopped manually.",
+ "defaultTTLHelperText_one": "Workspaces will default to stopping after {{count}} hour.",
+ "defaultTTLHelperText_other": "Workspaces will default to stopping after {{count}} hours.",
"maxTTLMaxError": "Please enter a limit that is less than or equal to 168 hours (7 days).",
"maxTTLMinError": "Maximum time until auto-stop must not be less than 0.",
- "maxTTLHelperText_zero": "Workspaces created from this template may run indefinitely.",
- "maxTTLHelperText_one": "Workspaces created from this template must stop within {{count}} hour of starting.",
- "maxTTLHelperText_other": "Workspaces created from this template must stop within {{count}} hours of starting.",
+ "maxTTLHelperText_zero": "Workspaces may run indefinitely.",
+ "maxTTLHelperText_one": "Workspaces must stop within 1 hour of starting.",
+ "maxTTLHelperText_other": "Workspaces must stop within {{count}} hours of starting.",
"allowUserCancelWorkspaceJobsLabel": "Allow users to cancel in-progress workspace jobs.",
"allowUserCancelWorkspaceJobsNotice": "Depending on your template, canceling builds may leave workspaces in an unhealthy state. This option isn't recommended for most use cases.",
"allowUsersCancelHelperText": "If checked, users may be able to corrupt their workspace.",
diff --git a/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx b/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx
index cc389bfa4558d..e65411271debc 100644
--- a/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx
+++ b/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx
@@ -17,7 +17,12 @@ import { useFormik } from "formik"
import { SelectedTemplate } from "pages/CreateWorkspacePage/SelectedTemplate"
import { FC } from "react"
import { useTranslation } from "react-i18next"
-import { nameValidator, getFormHelpers, onChangeTrimmed } from "util/formUtils"
+import {
+ nameValidator,
+ getFormHelpers,
+ onChangeTrimmed,
+ templateDisplayNameValidator,
+} from "util/formUtils"
import { CreateTemplateData } from "xServices/createTemplate/createTemplateXService"
import * as Yup from "yup"
import { WorkspaceBuildLogs } from "components/WorkspaceBuildLogs/WorkspaceBuildLogs"
@@ -25,14 +30,60 @@ import { HelpTooltip, HelpTooltipText } from "components/Tooltips/HelpTooltip"
import { LazyIconField } from "components/IconField/LazyIconField"
import { Maybe } from "components/Conditionals/Maybe"
import { useDashboard } from "components/Dashboard/DashboardProvider"
+import i18next from "i18next"
+import Link from "@material-ui/core/Link"
+
+const MAX_DESCRIPTION_CHAR_LIMIT = 128
+const MAX_TTL_DAYS = 7
+
+const TTLHelperText = ({
+ ttl,
+ translationName,
+}: {
+ ttl?: number
+ translationName: string
+}) => {
+ const { t } = useTranslation("createTemplatePage")
+ const count = typeof ttl !== "number" ? 0 : ttl
+ return (
+ // no helper text if ttl is negative - error will show once field is considered touched
+ = 0}>
+ {t(translationName, { count })}
+
+ )
+}
const validationSchema = Yup.object({
- name: nameValidator("Name"),
- display_name: Yup.string().optional(),
- description: Yup.string().optional(),
+ name: nameValidator(
+ i18next.t("form.fields.name", { ns: "createTemplatePage" }),
+ ),
+ display_name: templateDisplayNameValidator(
+ i18next.t("form.fields.displayName", {
+ ns: "createTemplatePage",
+ }),
+ ),
+ description: Yup.string().max(
+ MAX_DESCRIPTION_CHAR_LIMIT,
+ i18next.t("form.error.descriptionMax", { ns: "createTemplatePage" }),
+ ),
icon: Yup.string().optional(),
- default_ttl_hours: Yup.number(),
- max_ttl_hours: Yup.number(),
+ default_ttl_hours: Yup.number()
+ .integer()
+ .min(
+ 0,
+ i18next.t("form.error.defaultTTLMin", { ns: "templateSettingsPage" }),
+ )
+ .max(
+ 24 * MAX_TTL_DAYS /* 7 days in hours */,
+ i18next.t("form.error.defaultTTLMax", { ns: "templateSettingsPage" }),
+ ),
+ max_ttl_hours: Yup.number()
+ .integer()
+ .min(0, i18next.t("form.error.maxTTLMin", { ns: "templateSettingsPage" }))
+ .max(
+ 24 * MAX_TTL_DAYS /* 7 days in hours */,
+ i18next.t("form.error.maxTTLMax", { ns: "templateSettingsPage" }),
+ ),
allow_user_cancel_workspace_jobs: Yup.boolean(),
parameter_values_by_name: Yup.object().optional(),
})
@@ -110,6 +161,7 @@ export const CreateTemplateForm: FC = ({
})
const getFieldHelpers = getFormHelpers(form, error)
const { t } = useTranslation("createTemplatePage")
+ const { t: commonT } = useTranslation("common")
return (
-
+
,
+ )}
disabled={isSubmitting}
onChange={onChangeTrimmed(form)}
fullWidth
label={t("form.fields.autoStop")}
variant="outlined"
type="number"
- helperText={t("form.helperText.autoStop")}
/>
-
-
-
+
+ ) : (
+ <>
+ {commonT("licenseFieldTextHelper")}{" "}
+
+ {commonT("learnMore")}
+
+ .
+ >
+ ),
+ )}
+ disabled={isSubmitting || !canSetMaxTTL}
+ fullWidth
+ label={t("form.fields.maxTTL")}
+ variant="outlined"
+ type="number"
+ />
@@ -346,6 +415,10 @@ const useStyles = makeStyles((theme) => ({
},
},
+ ttlFields: {
+ width: "100%",
+ },
+
formSectionInfo: {
width: 312,
flexShrink: 0,
diff --git a/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx
index 7cea8f375aa33..bc115967672a1 100644
--- a/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx
+++ b/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx
@@ -23,7 +23,7 @@ import { Stack } from "components/Stack/Stack"
import Checkbox from "@material-ui/core/Checkbox"
import { HelpTooltip, HelpTooltipText } from "components/Tooltips/HelpTooltip"
import { makeStyles } from "@material-ui/core/styles"
-import { useDashboard } from "components/Dashboard/DashboardProvider"
+import Link from "@material-ui/core/Link"
const TTLHelperText = ({
ttl,
@@ -81,6 +81,7 @@ export interface TemplateSettingsForm {
onCancel: () => void
isSubmitting: boolean
error?: unknown
+ canSetMaxTTL: boolean
// Helpful to show field errors on Storybook
initialTouched?: FormikTouched
}
@@ -90,13 +91,11 @@ export const TemplateSettingsForm: FC = ({
onSubmit,
onCancel,
error,
+ canSetMaxTTL,
isSubmitting,
initialTouched,
}) => {
- const { entitlements } = useDashboard()
- const canSetMaxTTL =
- entitlements.features["advanced_template_scheduling"].enabled
-
+ const { t: commonT } = useTranslation("common")
const validationSchema = getValidationSchema()
const form: FormikContextType =
useFormik({
@@ -193,39 +192,49 @@ export const TemplateSettingsForm: FC = ({
title={t("schedule.title")}
description={t("schedule.description")}
>
- ,
- )}
- disabled={isSubmitting}
- fullWidth
- inputProps={{ min: 0, step: 1 }}
- label={t("defaultTtlLabel")}
- variant="outlined"
- type="number"
- />
-
-
+
,
)}
disabled={isSubmitting}
fullWidth
inputProps={{ min: 0, step: 1 }}
+ label={t("defaultTtlLabel")}
+ variant="outlined"
+ type="number"
+ />
+
+
+ ) : (
+ <>
+ {commonT("licenseFieldTextHelper")}{" "}
+
+ {commonT("learnMore")}
+
+ .
+ >
+ ),
+ )}
+ disabled={isSubmitting || !canSetMaxTTL}
+ fullWidth
+ inputProps={{ min: 0, step: 1 }}
label={t("maxTtlLabel")}
variant="outlined"
type="number"
/>
-
+
({
fontSize: theme.spacing(1.5),
color: theme.palette.text.secondary,
},
+
+ ttlFields: {
+ width: "100%",
+ },
}))
diff --git a/site/src/pages/TemplateSettingsPage/TemplateSettingsPage.tsx b/site/src/pages/TemplateSettingsPage/TemplateSettingsPage.tsx
index b00a56ac569d4..9fb935d731bab 100644
--- a/site/src/pages/TemplateSettingsPage/TemplateSettingsPage.tsx
+++ b/site/src/pages/TemplateSettingsPage/TemplateSettingsPage.tsx
@@ -1,4 +1,5 @@
import { useMachine } from "@xstate/react"
+import { useDashboard } from "components/Dashboard/DashboardProvider"
import { useOrganizationId } from "hooks/useOrganizationId"
import { FC } from "react"
import { Helmet } from "react-helmet-async"
@@ -27,6 +28,9 @@ export const TemplateSettingsPage: FC = () => {
saveTemplateSettingsError,
getTemplateError,
} = state.context
+ const { entitlements } = useDashboard()
+ const canSetMaxTTL =
+ entitlements.features["advanced_template_scheduling"].enabled
return (
<>
@@ -34,6 +38,7 @@ export const TemplateSettingsPage: FC = () => {
{pageTitle(t("title"))}
= (args) => (
@@ -17,10 +23,11 @@ const Template: Story = (args) => (
)
export const Example = Template.bind({})
-Example.args = {
- template: Mocks.MockTemplate,
- onSubmit: action("onSubmit"),
- onCancel: action("cancel"),
+Example.args = {}
+
+export const CantSetMaxTTL = Template.bind({})
+CantSetMaxTTL.args = {
+ canSetMaxTTL: false,
}
export const GetTemplateError = Template.bind({})
@@ -32,13 +39,10 @@ GetTemplateError.args = {
detail: "You do not have permission to access this resource.",
}),
},
- onSubmit: action("onSubmit"),
- onCancel: action("cancel"),
}
export const SaveTemplateSettingsError = Template.bind({})
SaveTemplateSettingsError.args = {
- template: Mocks.MockTemplate,
errors: {
saveTemplateSettingsError: makeMockApiError({
message: 'Template "test" already exists.',
@@ -53,6 +57,4 @@ SaveTemplateSettingsError.args = {
initialTouched: {
name: true,
},
- onSubmit: action("onSubmit"),
- onCancel: action("cancel"),
}
diff --git a/site/src/pages/TemplateSettingsPage/TemplateSettingsPageView.tsx b/site/src/pages/TemplateSettingsPage/TemplateSettingsPageView.tsx
index 6c514e1470b9a..14eec2798bb95 100644
--- a/site/src/pages/TemplateSettingsPage/TemplateSettingsPageView.tsx
+++ b/site/src/pages/TemplateSettingsPage/TemplateSettingsPageView.tsx
@@ -18,6 +18,7 @@ export interface TemplateSettingsPageViewProps {
saveTemplateSettingsError?: unknown
}
initialTouched?: ComponentProps["initialTouched"]
+ canSetMaxTTL: boolean
}
export const TemplateSettingsPageView: FC = ({
@@ -25,6 +26,7 @@ export const TemplateSettingsPageView: FC = ({
onCancel,
onSubmit,
isSubmitting,
+ canSetMaxTTL,
errors = {},
initialTouched,
}) => {
@@ -43,6 +45,7 @@ export const TemplateSettingsPageView: FC = ({
{template && (
<>
Date: Wed, 1 Mar 2023 17:55:44 +0000
Subject: [PATCH 12/24] fixup! Merge branch 'main' into dean/schedule-max-ttl
---
enterprise/coderd/templates_test.go | 1 +
1 file changed, 1 insertion(+)
diff --git a/enterprise/coderd/templates_test.go b/enterprise/coderd/templates_test.go
index 503637e7e441a..7c58bd9527514 100644
--- a/enterprise/coderd/templates_test.go
+++ b/enterprise/coderd/templates_test.go
@@ -61,6 +61,7 @@ func TestTemplates(t *testing.T) {
err := client.UpdateWorkspaceTTL(ctx, workspace3.ID, codersdk.UpdateWorkspaceTTLRequest{
TTLMillis: nil,
})
+ require.NoError(t, err)
updated, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
Name: template.Name,
From 651149b362458fdffac4e359d4ca1ae99a4a31d1 Mon Sep 17 00:00:00 2001
From: Dean Sheather
Date: Wed, 1 Mar 2023 18:57:28 +0000
Subject: [PATCH 13/24] fixup! Merge branch 'main' into dean/schedule-max-ttl
---
docs/admin/audit-logs.md | 22 +++++++++++-----------
1 file changed, 11 insertions(+), 11 deletions(-)
diff --git a/docs/admin/audit-logs.md b/docs/admin/audit-logs.md
index 5b9355b391e3d..fb4bb7a85de33 100644
--- a/docs/admin/audit-logs.md
+++ b/docs/admin/audit-logs.md
@@ -9,17 +9,17 @@ We track the following resources:
-| Resource | |
-| ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| APIKey
write | Field | Tracked |
---|
created_at | false |
expires_at | false |
hashed_secret | false |
id | false |
ip_address | false |
last_used | false |
lifetime_seconds | false |
login_type | false |
scope | false |
updated_at | false |
user_id | false |
|
-| Group
create, write, delete | Field | Tracked |
---|
avatar_url | true |
id | true |
members | true |
name | true |
organization_id | false |
quota_allowance | true |
|
-| GitSSHKey
create | Field | Tracked |
---|
created_at | false |
private_key | true |
public_key | true |
updated_at | false |
user_id | true |
|
-| License
create, delete | Field | Tracked |
---|
exp | true |
id | false |
jwt | false |
uploaded_at | true |
uuid | true |
|
-| Template
write, delete | Field | Tracked |
---|
active_version_id | true |
allow_user_cancel_workspace_jobs | true |
created_at | false |
created_by | true |
default_ttl | true |
deleted | false |
description | true |
display_name | true |
group_acl | true |
icon | true |
id | true |
is_private | true |
min_autostart_interval | true |
name | true |
organization_id | false |
provisioner | true |
updated_at | false |
user_acl | true |
|
-| TemplateVersion
create, write | Field | Tracked |
---|
created_at | false |
created_by | true |
git_auth_providers | false |
id | true |
job_id | false |
name | true |
organization_id | false |
readme | true |
template_id | true |
updated_at | false |
|
-| User
create, write, delete | Field | Tracked |
---|
avatar_url | false |
created_at | false |
deleted | true |
email | true |
hashed_password | true |
id | true |
last_seen_at | false |
login_type | false |
rbac_roles | true |
status | true |
updated_at | false |
username | true |
|
-| Workspace
create, write, delete | Field | Tracked |
---|
autostart_schedule | true |
created_at | false |
deleted | false |
id | true |
last_used_at | false |
name | true |
organization_id | false |
owner_id | true |
template_id | true |
ttl | true |
updated_at | false |
|
-| WorkspaceBuild
start, stop | Field | Tracked |
---|
build_number | false |
created_at | false |
daily_cost | false |
deadline | false |
id | false |
initiator_id | false |
job_id | false |
provisioner_state | false |
reason | false |
template_version_id | true |
transition | false |
updated_at | false |
workspace_id | false |
|
+| Resource | |
+| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| APIKey
write | Field | Tracked |
---|
created_at | false |
expires_at | false |
hashed_secret | false |
id | false |
ip_address | false |
last_used | false |
lifetime_seconds | false |
login_type | false |
scope | false |
updated_at | false |
user_id | false |
|
+| Group
create, write, delete | Field | Tracked |
---|
avatar_url | true |
id | true |
members | true |
name | true |
organization_id | false |
quota_allowance | true |
|
+| GitSSHKey
create | Field | Tracked |
---|
created_at | false |
private_key | true |
public_key | true |
updated_at | false |
user_id | true |
|
+| License
create, delete | Field | Tracked |
---|
exp | true |
id | false |
jwt | false |
uploaded_at | true |
uuid | true |
|
+| Template
write, delete | Field | Tracked |
---|
active_version_id | true |
allow_user_cancel_workspace_jobs | true |
created_at | false |
created_by | true |
default_ttl | true |
deleted | false |
description | true |
display_name | true |
group_acl | true |
icon | true |
id | true |
is_private | true |
max_ttl | true |
min_autostart_interval | true |
name | true |
organization_id | false |
provisioner | true |
updated_at | false |
user_acl | true |
|
+| TemplateVersion
create, write | Field | Tracked |
---|
created_at | false |
created_by | true |
git_auth_providers | false |
id | true |
job_id | false |
name | true |
organization_id | false |
readme | true |
template_id | true |
updated_at | false |
|
+| User
create, write, delete | Field | Tracked |
---|
avatar_url | false |
created_at | false |
deleted | true |
email | true |
hashed_password | true |
id | true |
last_seen_at | false |
login_type | false |
rbac_roles | true |
status | true |
updated_at | false |
username | true |
|
+| Workspace
create, write, delete | Field | Tracked |
---|
autostart_schedule | true |
created_at | false |
deleted | false |
id | true |
last_used_at | false |
name | true |
organization_id | false |
owner_id | true |
template_id | true |
ttl | true |
updated_at | false |
|
+| WorkspaceBuild
start, stop | Field | Tracked |
---|
build_number | false |
created_at | false |
daily_cost | false |
deadline | false |
id | false |
initiator_id | false |
job_id | false |
provisioner_state | false |
reason | false |
template_version_id | true |
transition | false |
updated_at | false |
workspace_id | false |
|
From 25f7d2c5a6671fc7e9ce8fa5e7daf0b9da1565d1 Mon Sep 17 00:00:00 2001
From: Dean Sheather
Date: Thu, 2 Mar 2023 15:05:40 +0000
Subject: [PATCH 14/24] chore: add test for activitybump max_ttl
---
cli/testdata/coder_list_--output_json.golden | 1 +
coderd/activitybump_test.go | 85 ++++++++++++++++++--
coderd/coderdtest/coderdtest.go | 67 +++++++--------
coderd/workspacebuilds.go | 1 +
codersdk/workspacebuilds.go | 1 +
5 files changed, 118 insertions(+), 37 deletions(-)
diff --git a/cli/testdata/coder_list_--output_json.golden b/cli/testdata/coder_list_--output_json.golden
index ac3761efd6dca..9a39ed4f9c3dc 100644
--- a/cli/testdata/coder_list_--output_json.golden
+++ b/cli/testdata/coder_list_--output_json.golden
@@ -39,6 +39,7 @@
"reason": "initiator",
"resources": [],
"deadline": "[timestamp]",
+ "max_deadline": null,
"status": "running",
"daily_cost": 0
},
diff --git a/coderd/activitybump_test.go b/coderd/activitybump_test.go
index fe8c947dcea62..d9568a3308c8f 100644
--- a/coderd/activitybump_test.go
+++ b/coderd/activitybump_test.go
@@ -5,24 +5,42 @@ import (
"testing"
"time"
+ "github.com/google/uuid"
"github.com/stretchr/testify/require"
"cdr.dev/slog/sloggers/slogtest"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/coderd/database"
+ "github.com/coder/coder/coderd/provisionerdserver"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/testutil"
)
+type mockTemplateScheduleStore struct {
+ getFn func(ctx context.Context, db database.Store, templateID uuid.UUID) (provisionerdserver.TemplateScheduleOptions, error)
+}
+
+var _ provisionerdserver.TemplateScheduleStore = mockTemplateScheduleStore{}
+
+func (m mockTemplateScheduleStore) GetTemplateScheduleOptions(ctx context.Context, db database.Store, templateID uuid.UUID) (provisionerdserver.TemplateScheduleOptions, error) {
+ return m.getFn(ctx, db, templateID)
+}
+
+func (mockTemplateScheduleStore) SetTemplateScheduleOptions(ctx context.Context, db database.Store, template database.Template, options provisionerdserver.TemplateScheduleOptions) (database.Template, error) {
+ return provisionerdserver.NewAGPLTemplateScheduleStore().SetTemplateScheduleOptions(ctx, db, template, options)
+}
+
func TestWorkspaceActivityBump(t *testing.T) {
t.Parallel()
ctx := context.Background()
- const ttl = time.Minute
-
- setupActivityTest := func(t *testing.T) (client *codersdk.Client, workspace codersdk.Workspace, assertBumped func(want bool)) {
- ttlMillis := int64(ttl / time.Millisecond)
+ setupActivityTest := func(t *testing.T, maxDeadline ...time.Duration) (client *codersdk.Client, workspace codersdk.Workspace, assertBumped func(want bool)) {
+ const ttl = time.Minute
+ maxTTL := time.Duration(0)
+ if len(maxDeadline) > 0 {
+ maxTTL = maxDeadline[0]
+ }
client = coderdtest.New(t, &coderdtest.Options{
AppHostname: proxyTestSubdomainRaw,
@@ -30,9 +48,19 @@ func TestWorkspaceActivityBump(t *testing.T) {
// Agent stats trigger the activity bump, so we want to report
// very frequently in tests.
AgentStatsRefreshInterval: time.Millisecond * 100,
+ TemplateScheduleStore: mockTemplateScheduleStore{
+ getFn: func(ctx context.Context, db database.Store, templateID uuid.UUID) (provisionerdserver.TemplateScheduleOptions, error) {
+ return provisionerdserver.TemplateScheduleOptions{
+ UserSchedulingEnabled: true,
+ DefaultTTL: ttl,
+ MaxTTL: maxTTL,
+ }, nil
+ },
+ },
})
user := coderdtest.CreateFirstUser(t, client)
+ ttlMillis := int64(ttl / time.Millisecond)
workspace = createWorkspaceWithApps(t, client, user.OrganizationID, "", 1234, func(cwr *codersdk.CreateWorkspaceRequest) {
cwr.TTLMillis = &ttlMillis
})
@@ -42,10 +70,21 @@ func TestWorkspaceActivityBump(t *testing.T) {
require.NoError(t, err)
require.WithinDuration(t,
time.Now().Add(time.Duration(ttlMillis)*time.Millisecond),
- workspace.LatestBuild.Deadline.Time, testutil.WaitMedium,
+ workspace.LatestBuild.Deadline.Time,
+ testutil.WaitMedium,
)
firstDeadline := workspace.LatestBuild.Deadline.Time
+ if maxTTL != 0 {
+ require.WithinDuration(t,
+ time.Now().Add(maxTTL),
+ workspace.LatestBuild.MaxDeadline.Time,
+ testutil.WaitMedium,
+ )
+ } else {
+ require.True(t, workspace.LatestBuild.MaxDeadline.Time.IsZero())
+ }
+
_ = coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
return client, workspace, func(want bool) {
@@ -74,6 +113,12 @@ func TestWorkspaceActivityBump(t *testing.T) {
"deadline %v never updated", firstDeadline,
)
+ // If the workspace has a max deadline, the deadline must not exceed
+ // it.
+ if maxTTL != 0 && database.Now().Add(ttl).After(workspace.LatestBuild.MaxDeadline.Time) {
+ require.Equal(t, workspace.LatestBuild.Deadline.Time, workspace.LatestBuild.MaxDeadline.Time)
+ return
+ }
require.WithinDuration(t, database.Now().Add(ttl), workspace.LatestBuild.Deadline.Time, 3*time.Second)
}
}
@@ -111,4 +156,34 @@ func TestWorkspaceActivityBump(t *testing.T) {
assertBumped(false)
})
+
+ t.Run("NotExceedMaxDeadline", func(t *testing.T) {
+ t.Parallel()
+
+ // Set the max deadline to be in 61 seconds. We bump by 1 minute, so we
+ // should expect the deadline to match the max deadline exactly.
+ client, workspace, assertBumped := setupActivityTest(t, 61*time.Second)
+
+ // Bump by dialing the workspace and sending traffic.
+ resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
+ conn, err := client.DialWorkspaceAgent(ctx, resources[0].Agents[0].ID, &codersdk.DialWorkspaceAgentOptions{
+ Logger: slogtest.Make(t, nil),
+ })
+ require.NoError(t, err)
+ defer conn.Close()
+
+ // Must send network traffic after a few seconds to surpass bump threshold.
+ time.Sleep(time.Second * 3)
+ sshConn, err := conn.SSHClient(ctx)
+ require.NoError(t, err)
+ _ = sshConn.Close()
+
+ assertBumped(true)
+
+ // Double check that the workspace build's deadline is equal to the
+ // max deadline.
+ workspace, err = client.Workspace(ctx, workspace.ID)
+ require.NoError(t, err)
+ require.Equal(t, workspace.LatestBuild.Deadline.Time, workspace.LatestBuild.MaxDeadline.Time)
+ })
}
diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go
index 7f94e852badc8..9a3d968b23e6f 100644
--- a/coderd/coderdtest/coderdtest.go
+++ b/coderd/coderdtest/coderdtest.go
@@ -65,6 +65,7 @@ import (
"github.com/coder/coder/coderd/gitsshkey"
"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/coderd/httpmw"
+ "github.com/coder/coder/coderd/provisionerdserver"
"github.com/coder/coder/coderd/rbac"
"github.com/coder/coder/coderd/telemetry"
"github.com/coder/coder/coderd/updatecheck"
@@ -85,22 +86,23 @@ type Options struct {
// AccessURL denotes a custom access URL. By default we use the httptest
// server's URL. Setting this may result in unexpected behavior (especially
// with running agents).
- AccessURL *url.URL
- AppHostname string
- AWSCertificates awsidentity.Certificates
- Authorizer rbac.Authorizer
- AzureCertificates x509.VerifyOptions
- GithubOAuth2Config *coderd.GithubOAuth2Config
- RealIPConfig *httpmw.RealIPConfig
- OIDCConfig *coderd.OIDCConfig
- GoogleTokenValidator *idtoken.Validator
- SSHKeygenAlgorithm gitsshkey.Algorithm
- AutobuildTicker <-chan time.Time
- AutobuildStats chan<- executor.Stats
- Auditor audit.Auditor
- TLSCertificates []tls.Certificate
- GitAuthConfigs []*gitauth.Config
- TrialGenerator func(context.Context, string) error
+ AccessURL *url.URL
+ AppHostname string
+ AWSCertificates awsidentity.Certificates
+ Authorizer rbac.Authorizer
+ AzureCertificates x509.VerifyOptions
+ GithubOAuth2Config *coderd.GithubOAuth2Config
+ RealIPConfig *httpmw.RealIPConfig
+ OIDCConfig *coderd.OIDCConfig
+ GoogleTokenValidator *idtoken.Validator
+ SSHKeygenAlgorithm gitsshkey.Algorithm
+ AutobuildTicker <-chan time.Time
+ AutobuildStats chan<- executor.Stats
+ Auditor audit.Auditor
+ TLSCertificates []tls.Certificate
+ GitAuthConfigs []*gitauth.Config
+ TrialGenerator func(context.Context, string) error
+ TemplateScheduleStore provisionerdserver.TemplateScheduleStore
// All rate limits default to -1 (unlimited) in tests if not set.
APIRateLimit int
@@ -287,22 +289,23 @@ func NewOptions(t *testing.T, options *Options) (func(http.Handler), context.Can
Pubsub: options.Pubsub,
GitAuthConfigs: options.GitAuthConfigs,
- Auditor: options.Auditor,
- AWSCertificates: options.AWSCertificates,
- AzureCertificates: options.AzureCertificates,
- GithubOAuth2Config: options.GithubOAuth2Config,
- RealIPConfig: options.RealIPConfig,
- OIDCConfig: options.OIDCConfig,
- GoogleTokenValidator: options.GoogleTokenValidator,
- SSHKeygenAlgorithm: options.SSHKeygenAlgorithm,
- DERPServer: derpServer,
- APIRateLimit: options.APIRateLimit,
- LoginRateLimit: options.LoginRateLimit,
- FilesRateLimit: options.FilesRateLimit,
- Authorizer: options.Authorizer,
- Telemetry: telemetry.NewNoop(),
- TLSCertificates: options.TLSCertificates,
- TrialGenerator: options.TrialGenerator,
+ Auditor: options.Auditor,
+ AWSCertificates: options.AWSCertificates,
+ AzureCertificates: options.AzureCertificates,
+ GithubOAuth2Config: options.GithubOAuth2Config,
+ RealIPConfig: options.RealIPConfig,
+ OIDCConfig: options.OIDCConfig,
+ GoogleTokenValidator: options.GoogleTokenValidator,
+ SSHKeygenAlgorithm: options.SSHKeygenAlgorithm,
+ DERPServer: derpServer,
+ APIRateLimit: options.APIRateLimit,
+ LoginRateLimit: options.LoginRateLimit,
+ FilesRateLimit: options.FilesRateLimit,
+ Authorizer: options.Authorizer,
+ Telemetry: telemetry.NewNoop(),
+ TemplateScheduleStore: options.TemplateScheduleStore,
+ TLSCertificates: options.TLSCertificates,
+ TrialGenerator: options.TrialGenerator,
DERPMap: &tailcfg.DERPMap{
Regions: map[int]*tailcfg.DERPRegion{
1: {
diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go
index 26176f5a0c93e..590f2df90eff1 100644
--- a/coderd/workspacebuilds.go
+++ b/coderd/workspacebuilds.go
@@ -1139,6 +1139,7 @@ func (api *API) convertWorkspaceBuild(
InitiatorUsername: initiator.Username,
Job: apiJob,
Deadline: codersdk.NewNullTime(build.Deadline, !build.Deadline.IsZero()),
+ MaxDeadline: codersdk.NewNullTime(build.MaxDeadline, !build.MaxDeadline.IsZero()),
Reason: codersdk.BuildReason(build.Reason),
Resources: apiResources,
Status: convertWorkspaceStatus(apiJob.Status, transition),
diff --git a/codersdk/workspacebuilds.go b/codersdk/workspacebuilds.go
index ab43379c28da8..2d8a70724d1d5 100644
--- a/codersdk/workspacebuilds.go
+++ b/codersdk/workspacebuilds.go
@@ -68,6 +68,7 @@ type WorkspaceBuild struct {
Reason BuildReason `db:"reason" json:"reason" enums:"initiator,autostart,autostop"`
Resources []WorkspaceResource `json:"resources"`
Deadline NullTime `json:"deadline,omitempty" format:"date-time"`
+ MaxDeadline NullTime `json:"max_deadline,omitempty" format:"date-time"`
Status WorkspaceStatus `json:"status" enums:"pending,starting,running,stopping,stopped,failed,canceling,canceled,deleting,deleted"`
DailyCost int32 `json:"daily_cost"`
}
From e3d8557b954a974d348903dcf0389d23fd8ed6e8 Mon Sep 17 00:00:00 2001
From: Dean Sheather
Date: Thu, 2 Mar 2023 15:40:26 +0000
Subject: [PATCH 15/24] chore: add tests for updating max_ttl on template
---
coderd/activitybump_test.go | 13 ++-
coderd/templates_test.go | 199 ++++++++++++++++++++++++++++++++++++
2 files changed, 210 insertions(+), 2 deletions(-)
diff --git a/coderd/activitybump_test.go b/coderd/activitybump_test.go
index d9568a3308c8f..9703ac59c713a 100644
--- a/coderd/activitybump_test.go
+++ b/coderd/activitybump_test.go
@@ -18,15 +18,24 @@ import (
type mockTemplateScheduleStore struct {
getFn func(ctx context.Context, db database.Store, templateID uuid.UUID) (provisionerdserver.TemplateScheduleOptions, error)
+ setFn func(ctx context.Context, db database.Store, template database.Template, options provisionerdserver.TemplateScheduleOptions) (database.Template, error)
}
var _ provisionerdserver.TemplateScheduleStore = mockTemplateScheduleStore{}
func (m mockTemplateScheduleStore) GetTemplateScheduleOptions(ctx context.Context, db database.Store, templateID uuid.UUID) (provisionerdserver.TemplateScheduleOptions, error) {
- return m.getFn(ctx, db, templateID)
+ if m.getFn != nil {
+ return m.getFn(ctx, db, templateID)
+ }
+
+ return provisionerdserver.NewAGPLTemplateScheduleStore().GetTemplateScheduleOptions(ctx, db, templateID)
}
-func (mockTemplateScheduleStore) SetTemplateScheduleOptions(ctx context.Context, db database.Store, template database.Template, options provisionerdserver.TemplateScheduleOptions) (database.Template, error) {
+func (m mockTemplateScheduleStore) SetTemplateScheduleOptions(ctx context.Context, db database.Store, template database.Template, options provisionerdserver.TemplateScheduleOptions) (database.Template, error) {
+ if m.setFn != nil {
+ return m.setFn(ctx, db, template, options)
+ }
+
return provisionerdserver.NewAGPLTemplateScheduleStore().SetTemplateScheduleOptions(ctx, db, template, options)
}
diff --git a/coderd/templates_test.go b/coderd/templates_test.go
index 092f6f75c9d99..90c5d0721e27c 100644
--- a/coderd/templates_test.go
+++ b/coderd/templates_test.go
@@ -3,6 +3,7 @@ package coderd_test
import (
"context"
"net/http"
+ "sync/atomic"
"testing"
"time"
@@ -15,6 +16,7 @@ import (
"github.com/coder/coder/coderd/audit"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/coderd/database"
+ "github.com/coder/coder/coderd/provisionerdserver"
"github.com/coder/coder/coderd/util/ptr"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/codersdk/agentsdk"
@@ -143,6 +145,95 @@ func TestPostTemplateByOrganization(t *testing.T) {
require.Contains(t, err.Error(), "Try logging in using 'coder login '.")
})
+ t.Run("MaxTTL", func(t *testing.T) {
+ t.Parallel()
+
+ const (
+ defaultTTL = 1 * time.Hour
+ maxTTL = 24 * time.Hour
+ )
+
+ t.Run("OK", func(t *testing.T) {
+ t.Parallel()
+
+ var setCalled int64
+ client := coderdtest.New(t, &coderdtest.Options{
+ TemplateScheduleStore: mockTemplateScheduleStore{
+ setFn: func(ctx context.Context, db database.Store, template database.Template, options provisionerdserver.TemplateScheduleOptions) (database.Template, error) {
+ atomic.AddInt64(&setCalled, 1)
+ require.Equal(t, maxTTL, options.MaxTTL)
+ template.DefaultTTL = int64(options.DefaultTTL)
+ template.MaxTTL = int64(options.MaxTTL)
+ return template, nil
+ },
+ },
+ })
+ user := coderdtest.CreateFirstUser(t, client)
+ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
+
+ ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
+ defer cancel()
+
+ got, err := client.CreateTemplate(ctx, user.OrganizationID, codersdk.CreateTemplateRequest{
+ Name: "testing",
+ VersionID: version.ID,
+ DefaultTTLMillis: ptr.Ref(int64(0)),
+ MaxTTLMillis: ptr.Ref(maxTTL.Milliseconds()),
+ })
+ require.NoError(t, err)
+
+ require.EqualValues(t, 1, atomic.LoadInt64(&setCalled))
+ require.EqualValues(t, 0, got.DefaultTTLMillis)
+ require.Equal(t, maxTTL.Milliseconds(), got.MaxTTLMillis)
+ })
+
+ t.Run("DefaultTTLBigger", func(t *testing.T) {
+ t.Parallel()
+
+ client := coderdtest.New(t, nil)
+ user := coderdtest.CreateFirstUser(t, client)
+ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
+
+ ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
+ defer cancel()
+
+ _, err := client.CreateTemplate(ctx, user.OrganizationID, codersdk.CreateTemplateRequest{
+ Name: "testing",
+ VersionID: version.ID,
+ DefaultTTLMillis: ptr.Ref((maxTTL * 2).Milliseconds()),
+ MaxTTLMillis: ptr.Ref(maxTTL.Milliseconds()),
+ })
+ require.Error(t, err)
+ var sdkErr *codersdk.Error
+ require.ErrorAs(t, err, &sdkErr)
+ require.Equal(t, http.StatusBadRequest, sdkErr.StatusCode())
+ require.Len(t, sdkErr.Validations, 1)
+ require.Equal(t, "default_ttl_ms", sdkErr.Validations[0].Field)
+ require.Contains(t, sdkErr.Validations[0].Detail, "Must be less than or equal to max_ttl_ms")
+ })
+
+ t.Run("IgnoredUnlicensed", func(t *testing.T) {
+ t.Parallel()
+
+ client := coderdtest.New(t, nil)
+ user := coderdtest.CreateFirstUser(t, client)
+ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
+
+ ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
+ defer cancel()
+
+ got, err := client.CreateTemplate(ctx, user.OrganizationID, codersdk.CreateTemplateRequest{
+ Name: "testing",
+ VersionID: version.ID,
+ DefaultTTLMillis: ptr.Ref(defaultTTL.Milliseconds()),
+ MaxTTLMillis: ptr.Ref(maxTTL.Milliseconds()),
+ })
+ require.NoError(t, err)
+ require.Equal(t, defaultTTL.Milliseconds(), got.DefaultTTLMillis)
+ require.Zero(t, got.MaxTTLMillis)
+ })
+ })
+
t.Run("NoVersion", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
@@ -345,6 +436,114 @@ func TestPatchTemplateMeta(t *testing.T) {
assert.Equal(t, updated.DefaultTTLMillis, template.DefaultTTLMillis)
})
+ t.Run("MaxTTL", func(t *testing.T) {
+ t.Parallel()
+
+ const (
+ defaultTTL = 1 * time.Hour
+ maxTTL = 24 * time.Hour
+ )
+
+ t.Run("OK", func(t *testing.T) {
+ t.Parallel()
+
+ var setCalled int64
+ client := coderdtest.New(t, &coderdtest.Options{
+ TemplateScheduleStore: mockTemplateScheduleStore{
+ setFn: func(ctx context.Context, db database.Store, template database.Template, options provisionerdserver.TemplateScheduleOptions) (database.Template, error) {
+ if atomic.AddInt64(&setCalled, 1) == 2 {
+ require.Equal(t, maxTTL, options.MaxTTL)
+ }
+ template.DefaultTTL = int64(options.DefaultTTL)
+ template.MaxTTL = int64(options.MaxTTL)
+ return template, nil
+ },
+ },
+ })
+ user := coderdtest.CreateFirstUser(t, client)
+ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
+ template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
+ ctr.DefaultTTLMillis = ptr.Ref(24 * time.Hour.Milliseconds())
+ })
+
+ ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
+ defer cancel()
+
+ got, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
+ Name: template.Name,
+ DisplayName: template.DisplayName,
+ Description: template.Description,
+ Icon: template.Icon,
+ DefaultTTLMillis: 0,
+ MaxTTLMillis: maxTTL.Milliseconds(),
+ AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
+ })
+ require.NoError(t, err)
+
+ require.EqualValues(t, 2, atomic.LoadInt64(&setCalled))
+ require.EqualValues(t, 0, got.DefaultTTLMillis)
+ require.Equal(t, maxTTL.Milliseconds(), got.MaxTTLMillis)
+ })
+
+ t.Run("DefaultTTLBigger", func(t *testing.T) {
+ t.Parallel()
+
+ client := coderdtest.New(t, nil)
+ user := coderdtest.CreateFirstUser(t, client)
+ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
+ template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
+ ctr.DefaultTTLMillis = ptr.Ref(24 * time.Hour.Milliseconds())
+ })
+
+ ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
+ defer cancel()
+
+ _, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
+ Name: template.Name,
+ DisplayName: template.DisplayName,
+ Description: template.Description,
+ Icon: template.Icon,
+ DefaultTTLMillis: (maxTTL * 2).Milliseconds(),
+ MaxTTLMillis: maxTTL.Milliseconds(),
+ AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
+ })
+ require.Error(t, err)
+ var sdkErr *codersdk.Error
+ require.ErrorAs(t, err, &sdkErr)
+ require.Equal(t, http.StatusBadRequest, sdkErr.StatusCode())
+ require.Len(t, sdkErr.Validations, 1)
+ require.Equal(t, "default_ttl_ms", sdkErr.Validations[0].Field)
+ require.Contains(t, sdkErr.Validations[0].Detail, "Must be less than or equal to max_ttl_ms")
+ })
+
+ t.Run("IgnoredUnlicensed", func(t *testing.T) {
+ t.Parallel()
+
+ client := coderdtest.New(t, nil)
+ user := coderdtest.CreateFirstUser(t, client)
+ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
+ template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
+ ctr.DefaultTTLMillis = ptr.Ref(24 * time.Hour.Milliseconds())
+ })
+
+ ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
+ defer cancel()
+
+ got, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
+ Name: template.Name,
+ DisplayName: template.DisplayName,
+ Description: template.Description,
+ Icon: template.Icon,
+ DefaultTTLMillis: defaultTTL.Milliseconds(),
+ MaxTTLMillis: maxTTL.Milliseconds(),
+ AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
+ })
+ require.NoError(t, err)
+ require.Equal(t, defaultTTL.Milliseconds(), got.DefaultTTLMillis)
+ require.Zero(t, got.MaxTTLMillis)
+ })
+ })
+
t.Run("NotModified", func(t *testing.T) {
t.Parallel()
From 2b029d8c4148783043b3f2507b10bb3d1959a3b6 Mon Sep 17 00:00:00 2001
From: Dean Sheather
Date: Thu, 2 Mar 2023 17:08:15 +0000
Subject: [PATCH 16/24] fixup! Merge branch 'main' into dean/schedule-max-ttl
---
coderd/apidoc/docs.go | 4 ++++
coderd/apidoc/swagger.json | 4 ++++
...own.sql => 000103_workspace_build_max_deadline.down.sql} | 0
...ne.up.sql => 000103_workspace_build_max_deadline.up.sql} | 0
docs/api/builds.md | 6 ++++++
docs/api/schemas.md | 4 ++++
docs/api/workspaces.md | 4 ++++
site/src/api/typesGenerated.ts | 1 +
8 files changed, 23 insertions(+)
rename coderd/database/migrations/{000102_workspace_build_max_deadline.down.sql => 000103_workspace_build_max_deadline.down.sql} (100%)
rename coderd/database/migrations/{000102_workspace_build_max_deadline.up.sql => 000103_workspace_build_max_deadline.up.sql} (100%)
diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go
index 48e956a4e291b..4d2bfeadf4dd5 100644
--- a/coderd/apidoc/docs.go
+++ b/coderd/apidoc/docs.go
@@ -8592,6 +8592,10 @@ const docTemplate = `{
"job": {
"$ref": "#/definitions/codersdk.ProvisionerJob"
},
+ "max_deadline": {
+ "type": "string",
+ "format": "date-time"
+ },
"reason": {
"enum": [
"initiator",
diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json
index dcc43d1714ba4..35a3e0bd3e582 100644
--- a/coderd/apidoc/swagger.json
+++ b/coderd/apidoc/swagger.json
@@ -7739,6 +7739,10 @@
"job": {
"$ref": "#/definitions/codersdk.ProvisionerJob"
},
+ "max_deadline": {
+ "type": "string",
+ "format": "date-time"
+ },
"reason": {
"enum": ["initiator", "autostart", "autostop"],
"allOf": [
diff --git a/coderd/database/migrations/000102_workspace_build_max_deadline.down.sql b/coderd/database/migrations/000103_workspace_build_max_deadline.down.sql
similarity index 100%
rename from coderd/database/migrations/000102_workspace_build_max_deadline.down.sql
rename to coderd/database/migrations/000103_workspace_build_max_deadline.down.sql
diff --git a/coderd/database/migrations/000102_workspace_build_max_deadline.up.sql b/coderd/database/migrations/000103_workspace_build_max_deadline.up.sql
similarity index 100%
rename from coderd/database/migrations/000102_workspace_build_max_deadline.up.sql
rename to coderd/database/migrations/000103_workspace_build_max_deadline.up.sql
diff --git a/docs/api/builds.md b/docs/api/builds.md
index 343bac7312b96..62c901971c79f 100644
--- a/docs/api/builds.md
+++ b/docs/api/builds.md
@@ -49,6 +49,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam
},
"worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b"
},
+ "max_deadline": "2019-08-24T14:15:22Z",
"reason": "initiator",
"resources": [
{
@@ -195,6 +196,7 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild} \
},
"worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b"
},
+ "max_deadline": "2019-08-24T14:15:22Z",
"reason": "initiator",
"resources": [
{
@@ -708,6 +710,7 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/sta
},
"worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b"
},
+ "max_deadline": "2019-08-24T14:15:22Z",
"reason": "initiator",
"resources": [
{
@@ -859,6 +862,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace}/builds \
},
"worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b"
},
+ "max_deadline": "2019-08-24T14:15:22Z",
"reason": "initiator",
"resources": [
{
@@ -983,6 +987,7 @@ Status Code **200**
| `»» tags` | object | false | | |
| `»»» [any property]` | string | false | | |
| `»» worker_id` | string(uuid) | false | | |
+| `» max_deadline` | string(date-time) | false | | |
| `» reason` | [codersdk.BuildReason](schemas.md#codersdkbuildreason) | false | | |
| `» resources` | array | false | | |
| `»» agents` | array | false | | |
@@ -1175,6 +1180,7 @@ curl -X POST http://coder-server:8080/api/v2/workspaces/{workspace}/builds \
},
"worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b"
},
+ "max_deadline": "2019-08-24T14:15:22Z",
"reason": "initiator",
"resources": [
{
diff --git a/docs/api/schemas.md b/docs/api/schemas.md
index 9c2fbcdc2226c..a7a86d05364bf 100644
--- a/docs/api/schemas.md
+++ b/docs/api/schemas.md
@@ -5304,6 +5304,7 @@ Parameter represents a set value for the scope.
},
"worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b"
},
+ "max_deadline": "2019-08-24T14:15:22Z",
"reason": "initiator",
"resources": [
{
@@ -5762,6 +5763,7 @@ Parameter represents a set value for the scope.
},
"worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b"
},
+ "max_deadline": "2019-08-24T14:15:22Z",
"reason": "initiator",
"resources": [
{
@@ -5865,6 +5867,7 @@ Parameter represents a set value for the scope.
| `initiator_id` | string | false | | |
| `initiator_name` | string | false | | |
| `job` | [codersdk.ProvisionerJob](#codersdkprovisionerjob) | false | | |
+| `max_deadline` | string | false | | |
| `reason` | [codersdk.BuildReason](#codersdkbuildreason) | false | | |
| `resources` | array of [codersdk.WorkspaceResource](#codersdkworkspaceresource) | false | | |
| `status` | [codersdk.WorkspaceStatus](#codersdkworkspacestatus) | false | | |
@@ -6126,6 +6129,7 @@ Parameter represents a set value for the scope.
},
"worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b"
},
+ "max_deadline": "2019-08-24T14:15:22Z",
"reason": "initiator",
"resources": [
{
diff --git a/docs/api/workspaces.md b/docs/api/workspaces.md
index 67e1f8c0125f7..79b1d707b7639 100644
--- a/docs/api/workspaces.md
+++ b/docs/api/workspaces.md
@@ -81,6 +81,7 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/member
},
"worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b"
},
+ "max_deadline": "2019-08-24T14:15:22Z",
"reason": "initiator",
"resources": [
{
@@ -246,6 +247,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam
},
"worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b"
},
+ "max_deadline": "2019-08-24T14:15:22Z",
"reason": "initiator",
"resources": [
{
@@ -434,6 +436,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces \
},
"worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b"
},
+ "max_deadline": "2019-08-24T14:15:22Z",
"reason": "initiator",
"resources": [
{
@@ -596,6 +599,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace} \
},
"worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b"
},
+ "max_deadline": "2019-08-24T14:15:22Z",
"reason": "initiator",
"resources": [
{
diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts
index b90ce766b3e50..9dfdd37dfceef 100644
--- a/site/src/api/typesGenerated.ts
+++ b/site/src/api/typesGenerated.ts
@@ -1043,6 +1043,7 @@ export interface WorkspaceBuild {
readonly reason: BuildReason
readonly resources: WorkspaceResource[]
readonly deadline?: string
+ readonly max_deadline?: string
readonly status: WorkspaceStatus
readonly daily_cost: number
}
From e60919e05cedc38f6cb4b88554e5b256ec04dc39 Mon Sep 17 00:00:00 2001
From: Dean Sheather
Date: Thu, 2 Mar 2023 17:13:27 +0000
Subject: [PATCH 17/24] chore: fix security.yaml not having protoc
---
.github/workflows/ci.yaml | 5 +++--
.github/workflows/security.yaml | 16 ++++++++++++++++
2 files changed, 19 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 4753aafeef26c..48eabc80c039e 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -186,8 +186,9 @@ jobs:
- name: Install Protoc
run: |
- # protoc must be in lockstep with our dogfood Dockerfile
- # or the version in the comments will differ.
+ # protoc must be in lockstep with our dogfood Dockerfile or the
+ # version in the comments will differ. This is also defined in
+ # security.yaml
set -x
cd dogfood
DOCKER_BUILDKIT=1 docker build . --target proto -t protoc
diff --git a/.github/workflows/security.yaml b/.github/workflows/security.yaml
index 5689825374b37..ab1e27c225fa7 100644
--- a/.github/workflows/security.yaml
+++ b/.github/workflows/security.yaml
@@ -94,6 +94,22 @@ jobs:
- name: Install yq
run: go run github.com/mikefarah/yq/v4@v4.30.6
+ - name: Install protoc-gen-go
+ run: go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.26
+ - name: Install protoc-gen-go-drpc
+ run: go install storj.io/drpc/cmd/protoc-gen-go-drpc@v0.0.26
+ - name: Install Protoc
+ run: |
+ # protoc must be in lockstep with our dogfood Dockerfile or the
+ # version in the comments will differ. This is also defined in
+ # ci.yaml.
+ set -x
+ cd dogfood
+ DOCKER_BUILDKIT=1 docker build . --target proto -t protoc
+ protoc_path=/usr/local/bin/protoc
+ docker run --rm --entrypoint cat protoc /tmp/bin/protoc > $protoc_path
+ chmod +x $protoc_path
+ protoc --version
- name: Build Coder linux amd64 Docker image
id: build
From 351b708f492e83f4f54271ff836c5f63ff86ab2b Mon Sep 17 00:00:00 2001
From: Dean Sheather
Date: Thu, 2 Mar 2023 18:18:27 +0000
Subject: [PATCH 18/24] fixup! Merge branch 'main' into dean/schedule-max-ttl
---
...line.down.sql => 000104_workspace_build_max_deadline.down.sql} | 0
...deadline.up.sql => 000104_workspace_build_max_deadline.up.sql} | 0
2 files changed, 0 insertions(+), 0 deletions(-)
rename coderd/database/migrations/{000103_workspace_build_max_deadline.down.sql => 000104_workspace_build_max_deadline.down.sql} (100%)
rename coderd/database/migrations/{000103_workspace_build_max_deadline.up.sql => 000104_workspace_build_max_deadline.up.sql} (100%)
diff --git a/coderd/database/migrations/000103_workspace_build_max_deadline.down.sql b/coderd/database/migrations/000104_workspace_build_max_deadline.down.sql
similarity index 100%
rename from coderd/database/migrations/000103_workspace_build_max_deadline.down.sql
rename to coderd/database/migrations/000104_workspace_build_max_deadline.down.sql
diff --git a/coderd/database/migrations/000103_workspace_build_max_deadline.up.sql b/coderd/database/migrations/000104_workspace_build_max_deadline.up.sql
similarity index 100%
rename from coderd/database/migrations/000103_workspace_build_max_deadline.up.sql
rename to coderd/database/migrations/000104_workspace_build_max_deadline.up.sql
From 847bc4c5078422fddb4f1895e2f46feec9c51a53 Mon Sep 17 00:00:00 2001
From: Dean Sheather
Date: Thu, 2 Mar 2023 18:42:11 +0000
Subject: [PATCH 19/24] chore: move schedule code to new package
---
cli/list.go | 2 +-
cli/schedule.go | 2 +-
cli/util.go | 2 +-
coderd/activitybump_test.go | 20 ++++++++--------
.../autobuild/executor/lifecycle_executor.go | 2 +-
.../executor/lifecycle_executor_test.go | 2 +-
coderd/coderd.go | 9 ++++----
coderd/coderdtest/coderdtest.go | 4 ++--
.../provisionerdserver/provisionerdserver.go | 3 ++-
.../provisionerdserver_test.go | 23 ++++++++++---------
.../schedule/schedule.go => schedule/cron.go} | 5 ++--
.../cron_test.go} | 2 +-
.../template.go} | 2 +-
coderd/templates.go | 6 ++---
coderd/templates_test.go | 6 ++---
coderd/workspaces.go | 2 +-
coderd/workspaces_test.go | 2 +-
enterprise/coderd/coderd.go | 6 ++---
enterprise/coderd/provisionerdaemons.go | 11 +++++----
19 files changed, 58 insertions(+), 53 deletions(-)
rename coderd/{autobuild/schedule/schedule.go => schedule/cron.go} (96%)
rename coderd/{autobuild/schedule/schedule_test.go => schedule/cron_test.go} (99%)
rename coderd/{provisionerdserver/templatescheduleoptions.go => schedule/template.go} (98%)
diff --git a/cli/list.go b/cli/list.go
index 510589eff64d0..33493cf807080 100644
--- a/cli/list.go
+++ b/cli/list.go
@@ -8,7 +8,7 @@ import (
"github.com/spf13/cobra"
"github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/coderd/autobuild/schedule"
+ "github.com/coder/coder/coderd/schedule"
"github.com/coder/coder/coderd/util/ptr"
"github.com/coder/coder/codersdk"
)
diff --git a/cli/schedule.go b/cli/schedule.go
index f774f5a51ad87..ff81b8e81dc50 100644
--- a/cli/schedule.go
+++ b/cli/schedule.go
@@ -10,7 +10,7 @@ import (
"golang.org/x/xerrors"
"github.com/coder/coder/cli/cliui"
- "github.com/coder/coder/coderd/autobuild/schedule"
+ "github.com/coder/coder/coderd/schedule"
"github.com/coder/coder/coderd/util/ptr"
"github.com/coder/coder/coderd/util/tz"
"github.com/coder/coder/codersdk"
diff --git a/cli/util.go b/cli/util.go
index 7ea5dd27e756b..777335d0a7d80 100644
--- a/cli/util.go
+++ b/cli/util.go
@@ -8,7 +8,7 @@ import (
"golang.org/x/xerrors"
- "github.com/coder/coder/coderd/autobuild/schedule"
+ "github.com/coder/coder/coderd/schedule"
"github.com/coder/coder/coderd/util/tz"
)
diff --git a/coderd/activitybump_test.go b/coderd/activitybump_test.go
index 9703ac59c713a..1548eddbb057c 100644
--- a/coderd/activitybump_test.go
+++ b/coderd/activitybump_test.go
@@ -11,32 +11,32 @@ import (
"cdr.dev/slog/sloggers/slogtest"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/provisionerdserver"
+ "github.com/coder/coder/coderd/schedule"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/testutil"
)
type mockTemplateScheduleStore struct {
- getFn func(ctx context.Context, db database.Store, templateID uuid.UUID) (provisionerdserver.TemplateScheduleOptions, error)
- setFn func(ctx context.Context, db database.Store, template database.Template, options provisionerdserver.TemplateScheduleOptions) (database.Template, error)
+ getFn func(ctx context.Context, db database.Store, templateID uuid.UUID) (schedule.TemplateScheduleOptions, error)
+ setFn func(ctx context.Context, db database.Store, template database.Template, options schedule.TemplateScheduleOptions) (database.Template, error)
}
-var _ provisionerdserver.TemplateScheduleStore = mockTemplateScheduleStore{}
+var _ schedule.TemplateScheduleStore = mockTemplateScheduleStore{}
-func (m mockTemplateScheduleStore) GetTemplateScheduleOptions(ctx context.Context, db database.Store, templateID uuid.UUID) (provisionerdserver.TemplateScheduleOptions, error) {
+func (m mockTemplateScheduleStore) GetTemplateScheduleOptions(ctx context.Context, db database.Store, templateID uuid.UUID) (schedule.TemplateScheduleOptions, error) {
if m.getFn != nil {
return m.getFn(ctx, db, templateID)
}
- return provisionerdserver.NewAGPLTemplateScheduleStore().GetTemplateScheduleOptions(ctx, db, templateID)
+ return schedule.NewAGPLTemplateScheduleStore().GetTemplateScheduleOptions(ctx, db, templateID)
}
-func (m mockTemplateScheduleStore) SetTemplateScheduleOptions(ctx context.Context, db database.Store, template database.Template, options provisionerdserver.TemplateScheduleOptions) (database.Template, error) {
+func (m mockTemplateScheduleStore) SetTemplateScheduleOptions(ctx context.Context, db database.Store, template database.Template, options schedule.TemplateScheduleOptions) (database.Template, error) {
if m.setFn != nil {
return m.setFn(ctx, db, template, options)
}
- return provisionerdserver.NewAGPLTemplateScheduleStore().SetTemplateScheduleOptions(ctx, db, template, options)
+ return schedule.NewAGPLTemplateScheduleStore().SetTemplateScheduleOptions(ctx, db, template, options)
}
func TestWorkspaceActivityBump(t *testing.T) {
@@ -58,8 +58,8 @@ func TestWorkspaceActivityBump(t *testing.T) {
// very frequently in tests.
AgentStatsRefreshInterval: time.Millisecond * 100,
TemplateScheduleStore: mockTemplateScheduleStore{
- getFn: func(ctx context.Context, db database.Store, templateID uuid.UUID) (provisionerdserver.TemplateScheduleOptions, error) {
- return provisionerdserver.TemplateScheduleOptions{
+ getFn: func(ctx context.Context, db database.Store, templateID uuid.UUID) (schedule.TemplateScheduleOptions, error) {
+ return schedule.TemplateScheduleOptions{
UserSchedulingEnabled: true,
DefaultTTL: ttl,
MaxTTL: maxTTL,
diff --git a/coderd/autobuild/executor/lifecycle_executor.go b/coderd/autobuild/executor/lifecycle_executor.go
index 6e72dff6af272..f6b4d0db12d87 100644
--- a/coderd/autobuild/executor/lifecycle_executor.go
+++ b/coderd/autobuild/executor/lifecycle_executor.go
@@ -10,10 +10,10 @@ import (
"golang.org/x/xerrors"
"cdr.dev/slog"
- "github.com/coder/coder/coderd/autobuild/schedule"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/database/dbauthz"
"github.com/coder/coder/coderd/provisionerdserver"
+ "github.com/coder/coder/coderd/schedule"
)
// Executor automatically starts or stops workspaces.
diff --git a/coderd/autobuild/executor/lifecycle_executor_test.go b/coderd/autobuild/executor/lifecycle_executor_test.go
index ac0ee6c6254f8..2548e317b69ff 100644
--- a/coderd/autobuild/executor/lifecycle_executor_test.go
+++ b/coderd/autobuild/executor/lifecycle_executor_test.go
@@ -11,9 +11,9 @@ import (
"github.com/google/uuid"
"github.com/coder/coder/coderd/autobuild/executor"
- "github.com/coder/coder/coderd/autobuild/schedule"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/coderd/database"
+ "github.com/coder/coder/coderd/schedule"
"github.com/coder/coder/coderd/util/ptr"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/provisioner/echo"
diff --git a/coderd/coderd.go b/coderd/coderd.go
index 9d82b709f98a6..450e1bc0746d7 100644
--- a/coderd/coderd.go
+++ b/coderd/coderd.go
@@ -51,6 +51,7 @@ import (
"github.com/coder/coder/coderd/metricscache"
"github.com/coder/coder/coderd/provisionerdserver"
"github.com/coder/coder/coderd/rbac"
+ "github.com/coder/coder/coderd/schedule"
"github.com/coder/coder/coderd/telemetry"
"github.com/coder/coder/coderd/tracing"
"github.com/coder/coder/coderd/updatecheck"
@@ -118,7 +119,7 @@ type Options struct {
DERPMap *tailcfg.DERPMap
SwaggerEndpoint bool
SetUserGroups func(ctx context.Context, tx database.Store, userID uuid.UUID, groupNames []string) error
- TemplateScheduleStore provisionerdserver.TemplateScheduleStore
+ TemplateScheduleStore schedule.TemplateScheduleStore
// APIRateLimit is the minutely throughput rate limit per user or ip.
// Setting a rate limit <0 will disable the rate limiter across the entire
@@ -211,7 +212,7 @@ func New(options *Options) *API {
options.SetUserGroups = func(context.Context, database.Store, uuid.UUID, []string) error { return nil }
}
if options.TemplateScheduleStore == nil {
- options.TemplateScheduleStore = provisionerdserver.NewAGPLTemplateScheduleStore()
+ options.TemplateScheduleStore = schedule.NewAGPLTemplateScheduleStore()
}
siteCacheDir := options.CacheDir
@@ -251,7 +252,7 @@ func New(options *Options) *API {
},
metricsCache: metricsCache,
Auditor: atomic.Pointer[audit.Auditor]{},
- TemplateScheduleStore: atomic.Pointer[provisionerdserver.TemplateScheduleStore]{},
+ TemplateScheduleStore: atomic.Pointer[schedule.TemplateScheduleStore]{},
Experiments: experiments,
}
if options.UpdateCheckOptions != nil {
@@ -726,7 +727,7 @@ type API struct {
WorkspaceClientCoordinateOverride atomic.Pointer[func(rw http.ResponseWriter) bool]
TailnetCoordinator atomic.Pointer[tailnet.Coordinator]
QuotaCommitter atomic.Pointer[proto.QuotaCommitter]
- TemplateScheduleStore atomic.Pointer[provisionerdserver.TemplateScheduleStore]
+ TemplateScheduleStore atomic.Pointer[schedule.TemplateScheduleStore]
HTTPAuth *HTTPAuthorizer
diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go
index 9a3d968b23e6f..e0b9a53d4e296 100644
--- a/coderd/coderdtest/coderdtest.go
+++ b/coderd/coderdtest/coderdtest.go
@@ -65,8 +65,8 @@ import (
"github.com/coder/coder/coderd/gitsshkey"
"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/coderd/provisionerdserver"
"github.com/coder/coder/coderd/rbac"
+ "github.com/coder/coder/coderd/schedule"
"github.com/coder/coder/coderd/telemetry"
"github.com/coder/coder/coderd/updatecheck"
"github.com/coder/coder/coderd/util/ptr"
@@ -102,7 +102,7 @@ type Options struct {
TLSCertificates []tls.Certificate
GitAuthConfigs []*gitauth.Config
TrialGenerator func(context.Context, string) error
- TemplateScheduleStore provisionerdserver.TemplateScheduleStore
+ TemplateScheduleStore schedule.TemplateScheduleStore
// All rate limits default to -1 (unlimited) in tests if not set.
APIRateLimit int
diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go
index b7538abd0e5df..5f8a6b1c7cb9d 100644
--- a/coderd/provisionerdserver/provisionerdserver.go
+++ b/coderd/provisionerdserver/provisionerdserver.go
@@ -28,6 +28,7 @@ import (
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/database/dbauthz"
"github.com/coder/coder/coderd/parameter"
+ "github.com/coder/coder/coderd/schedule"
"github.com/coder/coder/coderd/telemetry"
"github.com/coder/coder/coderd/util/slice"
"github.com/coder/coder/codersdk"
@@ -54,7 +55,7 @@ type Server struct {
Telemetry telemetry.Reporter
QuotaCommitter *atomic.Pointer[proto.QuotaCommitter]
Auditor *atomic.Pointer[audit.Auditor]
- TemplateScheduleStore *atomic.Pointer[TemplateScheduleStore]
+ TemplateScheduleStore *atomic.Pointer[schedule.TemplateScheduleStore]
AcquireJobDebounce time.Duration
}
diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go
index 518be8e25c55c..ad359d69ba4f3 100644
--- a/coderd/provisionerdserver/provisionerdserver_test.go
+++ b/coderd/provisionerdserver/provisionerdserver_test.go
@@ -18,6 +18,7 @@ import (
"github.com/coder/coder/coderd/database/dbfake"
"github.com/coder/coder/coderd/database/dbgen"
"github.com/coder/coder/coderd/provisionerdserver"
+ "github.com/coder/coder/coderd/schedule"
"github.com/coder/coder/coderd/telemetry"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/provisionerd/proto"
@@ -32,9 +33,9 @@ func mockAuditor() *atomic.Pointer[audit.Auditor] {
return ptr
}
-func testTemplateScheduleStore() *atomic.Pointer[provisionerdserver.TemplateScheduleStore] {
- ptr := &atomic.Pointer[provisionerdserver.TemplateScheduleStore]{}
- store := provisionerdserver.NewAGPLTemplateScheduleStore()
+func testTemplateScheduleStore() *atomic.Pointer[schedule.TemplateScheduleStore] {
+ ptr := &atomic.Pointer[schedule.TemplateScheduleStore]{}
+ store := schedule.NewAGPLTemplateScheduleStore()
ptr.Store(&store)
return ptr
}
@@ -890,9 +891,9 @@ func TestCompleteJob(t *testing.T) {
srv := setup(t, false)
- var store provisionerdserver.TemplateScheduleStore = mockTemplateScheduleStore{
- GetFn: func(_ context.Context, _ database.Store, _ uuid.UUID) (provisionerdserver.TemplateScheduleOptions, error) {
- return provisionerdserver.TemplateScheduleOptions{
+ var store schedule.TemplateScheduleStore = mockTemplateScheduleStore{
+ GetFn: func(_ context.Context, _ database.Store, _ uuid.UUID) (schedule.TemplateScheduleOptions, error) {
+ return schedule.TemplateScheduleOptions{
UserSchedulingEnabled: true,
DefaultTTL: c.templateDefaultTTL,
MaxTTL: c.templateMaxTTL,
@@ -1167,15 +1168,15 @@ func must[T any](value T, err error) T {
}
type mockTemplateScheduleStore struct {
- GetFn func(ctx context.Context, db database.Store, id uuid.UUID) (provisionerdserver.TemplateScheduleOptions, error)
+ GetFn func(ctx context.Context, db database.Store, id uuid.UUID) (schedule.TemplateScheduleOptions, error)
}
-var _ provisionerdserver.TemplateScheduleStore = mockTemplateScheduleStore{}
+var _ schedule.TemplateScheduleStore = mockTemplateScheduleStore{}
-func (mockTemplateScheduleStore) SetTemplateScheduleOptions(ctx context.Context, db database.Store, template database.Template, opts provisionerdserver.TemplateScheduleOptions) (database.Template, error) {
- return provisionerdserver.NewAGPLTemplateScheduleStore().SetTemplateScheduleOptions(ctx, db, template, opts)
+func (mockTemplateScheduleStore) SetTemplateScheduleOptions(ctx context.Context, db database.Store, template database.Template, opts schedule.TemplateScheduleOptions) (database.Template, error) {
+ return schedule.NewAGPLTemplateScheduleStore().SetTemplateScheduleOptions(ctx, db, template, opts)
}
-func (m mockTemplateScheduleStore) GetTemplateScheduleOptions(ctx context.Context, db database.Store, id uuid.UUID) (provisionerdserver.TemplateScheduleOptions, error) {
+func (m mockTemplateScheduleStore) GetTemplateScheduleOptions(ctx context.Context, db database.Store, id uuid.UUID) (schedule.TemplateScheduleOptions, error) {
return m.GetFn(ctx, db, id)
}
diff --git a/coderd/autobuild/schedule/schedule.go b/coderd/schedule/cron.go
similarity index 96%
rename from coderd/autobuild/schedule/schedule.go
rename to coderd/schedule/cron.go
index d13c367a2eb09..3da7ebb4b58d9 100644
--- a/coderd/autobuild/schedule/schedule.go
+++ b/coderd/schedule/cron.go
@@ -1,5 +1,6 @@
-// package schedule provides utilities for parsing and deserializing
-// cron-style expressions.
+// package schedule provides utilities for managing template and workspace
+// auto-start and auto-stop schedules. This includes utilities for parsing and
+// deserializing cron-style expressions.
package schedule
import (
diff --git a/coderd/autobuild/schedule/schedule_test.go b/coderd/schedule/cron_test.go
similarity index 99%
rename from coderd/autobuild/schedule/schedule_test.go
rename to coderd/schedule/cron_test.go
index ccba3e4f37773..9437cff4eed04 100644
--- a/coderd/autobuild/schedule/schedule_test.go
+++ b/coderd/schedule/cron_test.go
@@ -6,7 +6,7 @@ import (
"github.com/stretchr/testify/require"
- "github.com/coder/coder/coderd/autobuild/schedule"
+ "github.com/coder/coder/coderd/schedule"
)
func Test_Weekly(t *testing.T) {
diff --git a/coderd/provisionerdserver/templatescheduleoptions.go b/coderd/schedule/template.go
similarity index 98%
rename from coderd/provisionerdserver/templatescheduleoptions.go
rename to coderd/schedule/template.go
index 2387115c4f0b5..0d328837f3cf8 100644
--- a/coderd/provisionerdserver/templatescheduleoptions.go
+++ b/coderd/schedule/template.go
@@ -1,4 +1,4 @@
-package provisionerdserver
+package schedule
import (
"context"
diff --git a/coderd/templates.go b/coderd/templates.go
index 0d0d0b5e9ed83..aa4a6ddc239a2 100644
--- a/coderd/templates.go
+++ b/coderd/templates.go
@@ -18,8 +18,8 @@ import (
"github.com/coder/coder/coderd/database/dbauthz"
"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/coderd/provisionerdserver"
"github.com/coder/coder/coderd/rbac"
+ "github.com/coder/coder/coderd/schedule"
"github.com/coder/coder/coderd/telemetry"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/examples"
@@ -273,7 +273,7 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
return xerrors.Errorf("insert template: %s", err)
}
- dbTemplate, err = (*api.TemplateScheduleStore.Load()).SetTemplateScheduleOptions(ctx, tx, dbTemplate, provisionerdserver.TemplateScheduleOptions{
+ dbTemplate, err = (*api.TemplateScheduleStore.Load()).SetTemplateScheduleOptions(ctx, tx, dbTemplate, schedule.TemplateScheduleOptions{
UserSchedulingEnabled: true,
DefaultTTL: defaultTTL,
MaxTTL: maxTTL,
@@ -536,7 +536,7 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
defaultTTL := time.Duration(req.DefaultTTLMillis) * time.Millisecond
maxTTL := time.Duration(req.MaxTTLMillis) * time.Millisecond
if defaultTTL != time.Duration(template.DefaultTTL) || maxTTL != time.Duration(template.MaxTTL) {
- updated, err = (*api.TemplateScheduleStore.Load()).SetTemplateScheduleOptions(ctx, tx, updated, provisionerdserver.TemplateScheduleOptions{
+ updated, err = (*api.TemplateScheduleStore.Load()).SetTemplateScheduleOptions(ctx, tx, updated, schedule.TemplateScheduleOptions{
UserSchedulingEnabled: true,
DefaultTTL: defaultTTL,
MaxTTL: maxTTL,
diff --git a/coderd/templates_test.go b/coderd/templates_test.go
index 90c5d0721e27c..195169378d99d 100644
--- a/coderd/templates_test.go
+++ b/coderd/templates_test.go
@@ -16,7 +16,7 @@ import (
"github.com/coder/coder/coderd/audit"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/coderd/database"
- "github.com/coder/coder/coderd/provisionerdserver"
+ "github.com/coder/coder/coderd/schedule"
"github.com/coder/coder/coderd/util/ptr"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/codersdk/agentsdk"
@@ -159,7 +159,7 @@ func TestPostTemplateByOrganization(t *testing.T) {
var setCalled int64
client := coderdtest.New(t, &coderdtest.Options{
TemplateScheduleStore: mockTemplateScheduleStore{
- setFn: func(ctx context.Context, db database.Store, template database.Template, options provisionerdserver.TemplateScheduleOptions) (database.Template, error) {
+ setFn: func(ctx context.Context, db database.Store, template database.Template, options schedule.TemplateScheduleOptions) (database.Template, error) {
atomic.AddInt64(&setCalled, 1)
require.Equal(t, maxTTL, options.MaxTTL)
template.DefaultTTL = int64(options.DefaultTTL)
@@ -450,7 +450,7 @@ func TestPatchTemplateMeta(t *testing.T) {
var setCalled int64
client := coderdtest.New(t, &coderdtest.Options{
TemplateScheduleStore: mockTemplateScheduleStore{
- setFn: func(ctx context.Context, db database.Store, template database.Template, options provisionerdserver.TemplateScheduleOptions) (database.Template, error) {
+ setFn: func(ctx context.Context, db database.Store, template database.Template, options schedule.TemplateScheduleOptions) (database.Template, error) {
if atomic.AddInt64(&setCalled, 1) == 2 {
require.Equal(t, maxTTL, options.MaxTTL)
}
diff --git a/coderd/workspaces.go b/coderd/workspaces.go
index 6f1cf039e52fd..44032ec915772 100644
--- a/coderd/workspaces.go
+++ b/coderd/workspaces.go
@@ -18,12 +18,12 @@ import (
"cdr.dev/slog"
"github.com/coder/coder/coderd/audit"
- "github.com/coder/coder/coderd/autobuild/schedule"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/coderd/httpmw"
"github.com/coder/coder/coderd/provisionerdserver"
"github.com/coder/coder/coderd/rbac"
+ "github.com/coder/coder/coderd/schedule"
"github.com/coder/coder/coderd/searchquery"
"github.com/coder/coder/coderd/telemetry"
"github.com/coder/coder/coderd/tracing"
diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go
index 93c38941c6ba7..455487ddad21d 100644
--- a/coderd/workspaces_test.go
+++ b/coderd/workspaces_test.go
@@ -17,11 +17,11 @@ import (
"github.com/coder/coder/agent"
"github.com/coder/coder/coderd/audit"
- "github.com/coder/coder/coderd/autobuild/schedule"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/parameter"
"github.com/coder/coder/coderd/rbac"
+ "github.com/coder/coder/coderd/schedule"
"github.com/coder/coder/coderd/util/ptr"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/codersdk/agentsdk"
diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go
index 4571e98ffc077..8ddfb0bbd98d2 100644
--- a/enterprise/coderd/coderd.go
+++ b/enterprise/coderd/coderd.go
@@ -20,8 +20,8 @@ import (
agplaudit "github.com/coder/coder/coderd/audit"
"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/coderd/httpmw"
- "github.com/coder/coder/coderd/provisionerdserver"
"github.com/coder/coder/coderd/rbac"
+ "github.com/coder/coder/coderd/schedule"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/enterprise/coderd/license"
"github.com/coder/coder/enterprise/derpmesh"
@@ -315,10 +315,10 @@ func (api *API) updateEntitlements(ctx context.Context) error {
if changed, enabled := featureChanged(codersdk.FeatureAdvancedTemplateScheduling); changed {
if enabled {
store := &enterpriseTemplateScheduleStore{}
- ptr := provisionerdserver.TemplateScheduleStore(store)
+ ptr := schedule.TemplateScheduleStore(store)
api.AGPL.TemplateScheduleStore.Store(&ptr)
} else {
- store := provisionerdserver.NewAGPLTemplateScheduleStore()
+ store := schedule.NewAGPLTemplateScheduleStore()
api.AGPL.TemplateScheduleStore.Store(&store)
}
}
diff --git a/enterprise/coderd/provisionerdaemons.go b/enterprise/coderd/provisionerdaemons.go
index caddf10292e8d..a130bbeeb1c24 100644
--- a/enterprise/coderd/provisionerdaemons.go
+++ b/enterprise/coderd/provisionerdaemons.go
@@ -27,6 +27,7 @@ import (
"github.com/coder/coder/coderd/httpmw"
"github.com/coder/coder/coderd/provisionerdserver"
"github.com/coder/coder/coderd/rbac"
+ "github.com/coder/coder/coderd/schedule"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/provisionerd/proto"
)
@@ -306,15 +307,15 @@ func websocketNetConn(ctx context.Context, conn *websocket.Conn, msgType websock
type enterpriseTemplateScheduleStore struct{}
-var _ provisionerdserver.TemplateScheduleStore = &enterpriseTemplateScheduleStore{}
+var _ schedule.TemplateScheduleStore = &enterpriseTemplateScheduleStore{}
-func (*enterpriseTemplateScheduleStore) GetTemplateScheduleOptions(ctx context.Context, db database.Store, templateID uuid.UUID) (provisionerdserver.TemplateScheduleOptions, error) {
+func (*enterpriseTemplateScheduleStore) GetTemplateScheduleOptions(ctx context.Context, db database.Store, templateID uuid.UUID) (schedule.TemplateScheduleOptions, error) {
tpl, err := db.GetTemplateByID(ctx, templateID)
if err != nil {
- return provisionerdserver.TemplateScheduleOptions{}, err
+ return schedule.TemplateScheduleOptions{}, err
}
- return provisionerdserver.TemplateScheduleOptions{
+ return schedule.TemplateScheduleOptions{
// TODO: make configurable at template level
UserSchedulingEnabled: true,
DefaultTTL: time.Duration(tpl.DefaultTTL),
@@ -322,7 +323,7 @@ func (*enterpriseTemplateScheduleStore) GetTemplateScheduleOptions(ctx context.C
}, nil
}
-func (*enterpriseTemplateScheduleStore) SetTemplateScheduleOptions(ctx context.Context, db database.Store, tpl database.Template, opts provisionerdserver.TemplateScheduleOptions) (database.Template, error) {
+func (*enterpriseTemplateScheduleStore) SetTemplateScheduleOptions(ctx context.Context, db database.Store, tpl database.Template, opts schedule.TemplateScheduleOptions) (database.Template, error) {
template, err := db.UpdateTemplateScheduleByID(ctx, database.UpdateTemplateScheduleByIDParams{
ID: tpl.ID,
UpdatedAt: database.Now(),
From 0611794a648ac06e0b4f855a0da8fe2913aa88d6 Mon Sep 17 00:00:00 2001
From: Dean Sheather
Date: Thu, 2 Mar 2023 18:42:52 +0000
Subject: [PATCH 20/24] fixup! chore: move schedule code to new package
---
docs/admin/audit-logs.md | 22 +++++++++++-----------
1 file changed, 11 insertions(+), 11 deletions(-)
diff --git a/docs/admin/audit-logs.md b/docs/admin/audit-logs.md
index 0d376d755b377..f3950f0419174 100644
--- a/docs/admin/audit-logs.md
+++ b/docs/admin/audit-logs.md
@@ -9,17 +9,17 @@ We track the following resources:
-| Resource | |
-| ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| APIKey
write | Field | Tracked |
---|
created_at | false |
expires_at | false |
hashed_secret | false |
id | false |
ip_address | false |
last_used | false |
lifetime_seconds | false |
login_type | false |
scope | false |
token_name | false |
updated_at | false |
user_id | false |
|
-| Group
create, write, delete | Field | Tracked |
---|
avatar_url | true |
id | true |
members | true |
name | true |
organization_id | false |
quota_allowance | true |
|
-| GitSSHKey
create | Field | Tracked |
---|
created_at | false |
private_key | true |
public_key | true |
updated_at | false |
user_id | true |
|
-| License
create, delete | Field | Tracked |
---|
exp | true |
id | false |
jwt | false |
uploaded_at | true |
uuid | true |
|
-| Template
write, delete | Field | Tracked |
---|
active_version_id | true |
allow_user_cancel_workspace_jobs | true |
created_at | false |
created_by | true |
default_ttl | true |
deleted | false |
description | true |
display_name | true |
group_acl | true |
icon | true |
id | true |
is_private | true |
min_autostart_interval | true |
name | true |
organization_id | false |
provisioner | true |
updated_at | false |
user_acl | true |
|
-| TemplateVersion
create, write | Field | Tracked |
---|
created_at | false |
created_by | true |
git_auth_providers | false |
id | true |
job_id | false |
name | true |
organization_id | false |
readme | true |
template_id | true |
updated_at | false |
|
-| User
create, write, delete | Field | Tracked |
---|
avatar_url | false |
created_at | false |
deleted | true |
email | true |
hashed_password | true |
id | true |
last_seen_at | false |
login_type | false |
rbac_roles | true |
status | true |
updated_at | false |
username | true |
|
-| Workspace
create, write, delete | Field | Tracked |
---|
autostart_schedule | true |
created_at | false |
deleted | false |
id | true |
last_used_at | false |
name | true |
organization_id | false |
owner_id | true |
template_id | true |
ttl | true |
updated_at | false |
|
-| WorkspaceBuild
start, stop | Field | Tracked |
---|
build_number | false |
created_at | false |
daily_cost | false |
deadline | false |
id | false |
initiator_id | false |
job_id | false |
provisioner_state | false |
reason | false |
template_version_id | true |
transition | false |
updated_at | false |
workspace_id | false |
|
+| Resource | |
+| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| APIKey
write | Field | Tracked |
---|
created_at | false |
expires_at | false |
hashed_secret | false |
id | false |
ip_address | false |
last_used | false |
lifetime_seconds | false |
login_type | false |
scope | false |
token_name | false |
updated_at | false |
user_id | false |
|
+| Group
create, write, delete | Field | Tracked |
---|
avatar_url | true |
id | true |
members | true |
name | true |
organization_id | false |
quota_allowance | true |
|
+| GitSSHKey
create | Field | Tracked |
---|
created_at | false |
private_key | true |
public_key | true |
updated_at | false |
user_id | true |
|
+| License
create, delete | Field | Tracked |
---|
exp | true |
id | false |
jwt | false |
uploaded_at | true |
uuid | true |
|
+| Template
write, delete | Field | Tracked |
---|
active_version_id | true |
allow_user_cancel_workspace_jobs | true |
created_at | false |
created_by | true |
default_ttl | true |
deleted | false |
description | true |
display_name | true |
group_acl | true |
icon | true |
id | true |
is_private | true |
max_ttl | true |
min_autostart_interval | true |
name | true |
organization_id | false |
provisioner | true |
updated_at | false |
user_acl | true |
|
+| TemplateVersion
create, write | Field | Tracked |
---|
created_at | false |
created_by | true |
git_auth_providers | false |
id | true |
job_id | false |
name | true |
organization_id | false |
readme | true |
template_id | true |
updated_at | false |
|
+| User
create, write, delete | Field | Tracked |
---|
avatar_url | false |
created_at | false |
deleted | true |
email | true |
hashed_password | true |
id | true |
last_seen_at | false |
login_type | false |
rbac_roles | true |
status | true |
updated_at | false |
username | true |
|
+| Workspace
create, write, delete | Field | Tracked |
---|
autostart_schedule | true |
created_at | false |
deleted | false |
id | true |
last_used_at | false |
name | true |
organization_id | false |
owner_id | true |
template_id | true |
ttl | true |
updated_at | false |
|
+| WorkspaceBuild
start, stop | Field | Tracked |
---|
build_number | false |
created_at | false |
daily_cost | false |
deadline | false |
id | false |
initiator_id | false |
job_id | false |
provisioner_state | false |
reason | false |
template_version_id | true |
transition | false |
updated_at | false |
workspace_id | false |
|
From e44f589765062ffbeaa4edd21c3c4dcb1dc8e1b7 Mon Sep 17 00:00:00 2001
From: Dean Sheather
Date: Mon, 6 Mar 2023 18:25:41 +0000
Subject: [PATCH 21/24] chore: add alpha label to max_ttl
---
site/src/i18n/en/createTemplatePage.json | 2 +-
site/src/i18n/en/templateSettingsPage.json | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/site/src/i18n/en/createTemplatePage.json b/site/src/i18n/en/createTemplatePage.json
index 15696c4157598..567e17ca60dbd 100644
--- a/site/src/i18n/en/createTemplatePage.json
+++ b/site/src/i18n/en/createTemplatePage.json
@@ -27,7 +27,7 @@
"description": "Description",
"icon": "Icon",
"autoStop": "Default auto-stop",
- "maxTTL": "Max. Lifetime",
+ "maxTTL": "Max. Lifetime (alpha)",
"allowUsersToCancel": "Allow users to cancel in-progress workspace jobs"
},
"helperText": {
diff --git a/site/src/i18n/en/templateSettingsPage.json b/site/src/i18n/en/templateSettingsPage.json
index 89337d99b5664..bd0f35f42711d 100644
--- a/site/src/i18n/en/templateSettingsPage.json
+++ b/site/src/i18n/en/templateSettingsPage.json
@@ -5,7 +5,7 @@
"descriptionLabel": "Description",
"descriptionMaxError": "Please enter a description that is less than or equal to 128 characters.",
"defaultTtlLabel": "Default auto-stop",
- "maxTtlLabel": "Max. Lifetime",
+ "maxTtlLabel": "Max. Lifetime (alpha)",
"iconLabel": "Icon",
"formAriaLabel": "Template settings form",
"selectEmoji": "Select emoji",
From 3ce2bc3b32aead2ea2fe205c3f2c4f3bf3e0fd01 Mon Sep 17 00:00:00 2001
From: Bruno Quaresma
Date: Mon, 6 Mar 2023 20:20:25 +0000
Subject: [PATCH 22/24] Fix test
---
.../TemplateSettingsPage.test.tsx | 17 +++++++++++------
1 file changed, 11 insertions(+), 6 deletions(-)
diff --git a/site/src/pages/TemplateSettingsPage/TemplateSettingsPage.test.tsx b/site/src/pages/TemplateSettingsPage/TemplateSettingsPage.test.tsx
index 3facf3e44dda7..52b601a7c725e 100644
--- a/site/src/pages/TemplateSettingsPage/TemplateSettingsPage.test.tsx
+++ b/site/src/pages/TemplateSettingsPage/TemplateSettingsPage.test.tsx
@@ -121,12 +121,17 @@ describe("TemplateSettingsPage", () => {
await fillAndSubmitForm(validFormValues)
await waitFor(() => expect(API.updateTemplateMeta).toBeCalledTimes(1))
- expect(API.updateTemplateMeta).toBeCalledWith(
- "test-template",
- expect.objectContaining({
- ...validFormValues,
- default_ttl_ms: 3600000, // the default_ttl_ms to ms
- }),
+ await waitFor(() =>
+ expect(API.updateTemplateMeta).toBeCalledWith(
+ "test-template",
+ expect.objectContaining({
+ ...validFormValues,
+ // convert from the display value (hours) to ms
+ default_ttl_ms: validFormValues.default_ttl_ms * 3600000,
+ // this value is undefined if not entitled
+ max_ttl_ms: undefined,
+ }),
+ ),
)
})
From 4beff52f503308a016cdb1ee0fd199c1dac802e7 Mon Sep 17 00:00:00 2001
From: Dean Sheather
Date: Mon, 6 Mar 2023 21:00:01 +0000
Subject: [PATCH 23/24] fixup! Fix test
---
site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx b/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx
index 3139234904906..1b3b6b2210f62 100644
--- a/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx
+++ b/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx
@@ -30,7 +30,7 @@ import { HelpTooltip, HelpTooltipText } from "components/Tooltips/HelpTooltip"
import { LazyIconField } from "components/IconField/LazyIconField"
import { Maybe } from "components/Conditionals/Maybe"
import { useDashboard } from "components/Dashboard/DashboardProvider"
-import i18next, { t } from "i18next"
+import i18next from "i18next"
import Link from "@material-ui/core/Link"
import { FormFooter } from "components/FormFooter/FormFooter"
import {
From 5e97e9651393065ef0cf2831c245dfef06a07007 Mon Sep 17 00:00:00 2001
From: Bruno Quaresma
Date: Tue, 7 Mar 2023 14:02:51 +0000
Subject: [PATCH 24/24] Fix storybook
---
site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx | 7 ++-----
site/src/pages/CreateTemplatePage/CreateTemplatePage.tsx | 5 +++++
2 files changed, 7 insertions(+), 5 deletions(-)
diff --git a/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx b/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx
index 1b3b6b2210f62..f1af33a9d38cf 100644
--- a/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx
+++ b/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx
@@ -29,7 +29,6 @@ import { WorkspaceBuildLogs } from "components/WorkspaceBuildLogs/WorkspaceBuild
import { HelpTooltip, HelpTooltipText } from "components/Tooltips/HelpTooltip"
import { LazyIconField } from "components/IconField/LazyIconField"
import { Maybe } from "components/Conditionals/Maybe"
-import { useDashboard } from "components/Dashboard/DashboardProvider"
import i18next from "i18next"
import Link from "@material-ui/core/Link"
import { FormFooter } from "components/FormFooter/FormFooter"
@@ -142,6 +141,7 @@ export interface CreateTemplateFormProps {
error?: unknown
jobError?: string
logs?: ProvisionerJobLog[]
+ canSetMaxTTL: boolean
}
export const CreateTemplateForm: FC = ({
@@ -155,12 +155,9 @@ export const CreateTemplateForm: FC = ({
error,
jobError,
logs,
+ canSetMaxTTL,
}) => {
const styles = useStyles()
- const { entitlements } = useDashboard()
- const canSetMaxTTL =
- entitlements.features["advanced_template_scheduling"].enabled
-
const form = useFormik({
initialValues: getInitialValues(canSetMaxTTL, starterTemplate),
validationSchema,
diff --git a/site/src/pages/CreateTemplatePage/CreateTemplatePage.tsx b/site/src/pages/CreateTemplatePage/CreateTemplatePage.tsx
index dabe9628feb4b..4692bbd346b02 100644
--- a/site/src/pages/CreateTemplatePage/CreateTemplatePage.tsx
+++ b/site/src/pages/CreateTemplatePage/CreateTemplatePage.tsx
@@ -2,6 +2,7 @@ import { useMachine } from "@xstate/react"
import { isApiValidationError } from "api/errors"
import { AlertBanner } from "components/AlertBanner/AlertBanner"
import { Maybe } from "components/Conditionals/Maybe"
+import { useDashboard } from "components/Dashboard/DashboardProvider"
import { FullPageHorizontalForm } from "components/FullPageForm/FullPageHorizontalForm"
import { Loader } from "components/Loader/Loader"
import { Stack } from "components/Stack/Stack"
@@ -40,6 +41,9 @@ const CreateTemplatePage: FC = () => {
variables,
} = state.context
const shouldDisplayForm = !state.hasTag("loading")
+ const { entitlements } = useDashboard()
+ const canSetMaxTTL =
+ entitlements.features["advanced_template_scheduling"].enabled
const onCancel = () => {
navigate(-1)
@@ -63,6 +67,7 @@ const CreateTemplatePage: FC = () => {
{shouldDisplayForm && (