mirror of
https://github.com/coder/coder.git
synced 2025-07-15 22:20:27 +00:00
feat: change template max_ttl to default_ttl (#4843)
This commit is contained in:
@ -27,8 +27,7 @@ func templateCreate() *cobra.Command {
|
||||
directory string
|
||||
provisioner string
|
||||
parameterFile string
|
||||
maxTTL time.Duration
|
||||
minAutostartInterval time.Duration
|
||||
defaultTTL time.Duration
|
||||
)
|
||||
cmd := &cobra.Command{
|
||||
Use: "create [name]",
|
||||
@ -110,8 +109,7 @@ func templateCreate() *cobra.Command {
|
||||
createReq := codersdk.CreateTemplateRequest{
|
||||
Name: templateName,
|
||||
VersionID: job.ID,
|
||||
MaxTTLMillis: ptr.Ref(maxTTL.Milliseconds()),
|
||||
MinAutostartIntervalMillis: ptr.Ref(minAutostartInterval.Milliseconds()),
|
||||
DefaultTTLMillis: ptr.Ref(defaultTTL.Milliseconds()),
|
||||
}
|
||||
|
||||
_, err = client.CreateTemplate(cmd.Context(), organization.ID, createReq)
|
||||
@ -133,8 +131,7 @@ func templateCreate() *cobra.Command {
|
||||
cmd.Flags().StringVarP(&directory, "directory", "d", currentDirectory, "Specify the directory to create from")
|
||||
cmd.Flags().StringVarP(&provisioner, "test.provisioner", "", "terraform", "Customize the provisioner backend")
|
||||
cmd.Flags().StringVarP(¶meterFile, "parameter-file", "", "", "Specify a file path with parameter values.")
|
||||
cmd.Flags().DurationVarP(&maxTTL, "max-ttl", "", 24*time.Hour, "Specify a maximum TTL for workspaces created from this template.")
|
||||
cmd.Flags().DurationVarP(&minAutostartInterval, "min-autostart-interval", "", time.Hour, "Specify a minimum autostart interval for workspaces created from this template.")
|
||||
cmd.Flags().DurationVarP(&defaultTTL, "default-ttl", "", 24*time.Hour, "Specify a default TTL for workspaces created from this template.")
|
||||
// This is for testing!
|
||||
err := cmd.Flags().MarkHidden("test.provisioner")
|
||||
if err != nil {
|
||||
|
@ -52,8 +52,7 @@ func TestTemplateCreate(t *testing.T) {
|
||||
"my-template",
|
||||
"--directory", source,
|
||||
"--test.provisioner", string(database.ProvisionerTypeEcho),
|
||||
"--max-ttl", "24h",
|
||||
"--min-autostart-interval", "2h",
|
||||
"--default-ttl", "24h",
|
||||
}
|
||||
cmd, root := clitest.New(t, args...)
|
||||
clitest.SetupConfig(t, client, root)
|
||||
|
@ -16,8 +16,7 @@ func templateEdit() *cobra.Command {
|
||||
name string
|
||||
description string
|
||||
icon string
|
||||
maxTTL time.Duration
|
||||
minAutostartInterval time.Duration
|
||||
defaultTTL time.Duration
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -43,8 +42,7 @@ func templateEdit() *cobra.Command {
|
||||
Name: name,
|
||||
Description: description,
|
||||
Icon: icon,
|
||||
MaxTTLMillis: maxTTL.Milliseconds(),
|
||||
MinAutostartIntervalMillis: minAutostartInterval.Milliseconds(),
|
||||
DefaultTTLMillis: defaultTTL.Milliseconds(),
|
||||
}
|
||||
|
||||
_, err = client.UpdateTemplateMeta(cmd.Context(), template.ID, req)
|
||||
@ -59,8 +57,7 @@ func templateEdit() *cobra.Command {
|
||||
cmd.Flags().StringVarP(&name, "name", "", "", "Edit the template name")
|
||||
cmd.Flags().StringVarP(&description, "description", "", "", "Edit the template description")
|
||||
cmd.Flags().StringVarP(&icon, "icon", "", "", "Edit the template icon path")
|
||||
cmd.Flags().DurationVarP(&maxTTL, "max-ttl", "", 0, "Edit the template maximum time before shutdown - workspaces created from this template cannot stay running longer than this.")
|
||||
cmd.Flags().DurationVarP(&minAutostartInterval, "min-autostart-interval", "", 0, "Edit the template minimum autostart interval - workspaces created from this template must wait at least this long between autostarts.")
|
||||
cmd.Flags().DurationVarP(&defaultTTL, "default-ttl", "", 0, "Edit the template default time before shutdown - workspaces created from this template to this value.")
|
||||
cliui.AllowSkipPrompt(cmd)
|
||||
|
||||
return cmd
|
||||
|
@ -26,16 +26,14 @@ func TestTemplateEdit(t *testing.T) {
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
|
||||
ctr.Description = "original description"
|
||||
ctr.Icon = "/icons/default-icon.png"
|
||||
ctr.MaxTTLMillis = ptr.Ref(24 * time.Hour.Milliseconds())
|
||||
ctr.MinAutostartIntervalMillis = ptr.Ref(time.Hour.Milliseconds())
|
||||
ctr.DefaultTTLMillis = ptr.Ref(24 * time.Hour.Milliseconds())
|
||||
})
|
||||
|
||||
// Test the cli command.
|
||||
name := "new-template-name"
|
||||
desc := "lorem ipsum dolor sit amet et cetera"
|
||||
icon := "/icons/new-icon.png"
|
||||
maxTTL := 12 * time.Hour
|
||||
minAutostartInterval := time.Minute
|
||||
defaultTTL := 12 * time.Hour
|
||||
cmdArgs := []string{
|
||||
"templates",
|
||||
"edit",
|
||||
@ -43,8 +41,7 @@ func TestTemplateEdit(t *testing.T) {
|
||||
"--name", name,
|
||||
"--description", desc,
|
||||
"--icon", icon,
|
||||
"--max-ttl", maxTTL.String(),
|
||||
"--min-autostart-interval", minAutostartInterval.String(),
|
||||
"--default-ttl", defaultTTL.String(),
|
||||
}
|
||||
cmd, root := clitest.New(t, cmdArgs...)
|
||||
clitest.SetupConfig(t, client, root)
|
||||
@ -59,8 +56,7 @@ func TestTemplateEdit(t *testing.T) {
|
||||
assert.Equal(t, name, updated.Name)
|
||||
assert.Equal(t, desc, updated.Description)
|
||||
assert.Equal(t, icon, updated.Icon)
|
||||
assert.Equal(t, maxTTL.Milliseconds(), updated.MaxTTLMillis)
|
||||
assert.Equal(t, minAutostartInterval.Milliseconds(), updated.MinAutostartIntervalMillis)
|
||||
assert.Equal(t, defaultTTL.Milliseconds(), updated.DefaultTTLMillis)
|
||||
})
|
||||
|
||||
t.Run("NotModified", func(t *testing.T) {
|
||||
@ -72,8 +68,7 @@ func TestTemplateEdit(t *testing.T) {
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
|
||||
ctr.Description = "original description"
|
||||
ctr.Icon = "/icons/default-icon.png"
|
||||
ctr.MaxTTLMillis = ptr.Ref(24 * time.Hour.Milliseconds())
|
||||
ctr.MinAutostartIntervalMillis = ptr.Ref(time.Hour.Milliseconds())
|
||||
ctr.DefaultTTLMillis = ptr.Ref(24 * time.Hour.Milliseconds())
|
||||
})
|
||||
|
||||
// Test the cli command.
|
||||
@ -84,8 +79,7 @@ func TestTemplateEdit(t *testing.T) {
|
||||
"--name", template.Name,
|
||||
"--description", template.Description,
|
||||
"--icon", template.Icon,
|
||||
"--max-ttl", (time.Duration(template.MaxTTLMillis) * time.Millisecond).String(),
|
||||
"--min-autostart-interval", (time.Duration(template.MinAutostartIntervalMillis) * time.Millisecond).String(),
|
||||
"--default-ttl", (time.Duration(template.DefaultTTLMillis) * time.Millisecond).String(),
|
||||
}
|
||||
cmd, root := clitest.New(t, cmdArgs...)
|
||||
clitest.SetupConfig(t, client, root)
|
||||
@ -100,7 +94,6 @@ func TestTemplateEdit(t *testing.T) {
|
||||
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.MaxTTLMillis, updated.MaxTTLMillis)
|
||||
assert.Equal(t, template.MinAutostartIntervalMillis, updated.MinAutostartIntervalMillis)
|
||||
assert.Equal(t, template.DefaultTTLMillis, updated.DefaultTTLMillis)
|
||||
})
|
||||
}
|
||||
|
@ -57,8 +57,7 @@ type templateTableRow struct {
|
||||
Provisioner codersdk.ProvisionerType `table:"provisioner"`
|
||||
ActiveVersionID uuid.UUID `table:"active version id"`
|
||||
UsedBy string `table:"used by"`
|
||||
MaxTTL time.Duration `table:"max ttl"`
|
||||
MinAutostartInterval time.Duration `table:"min autostart"`
|
||||
DefaultTTL time.Duration `table:"default ttl"`
|
||||
}
|
||||
|
||||
// displayTemplates will return a table displaying all templates passed in.
|
||||
@ -75,8 +74,7 @@ func displayTemplates(filterColumns []string, templates ...codersdk.Template) (s
|
||||
Provisioner: template.Provisioner,
|
||||
ActiveVersionID: template.ActiveVersionID,
|
||||
UsedBy: cliui.Styles.Fuchsia.Render(formatActiveDevelopers(template.ActiveUserCount)),
|
||||
MaxTTL: (time.Duration(template.MaxTTLMillis) * time.Millisecond),
|
||||
MinAutostartInterval: (time.Duration(template.MinAutostartIntervalMillis) * time.Millisecond),
|
||||
DefaultTTL: (time.Duration(template.DefaultTTLMillis) * time.Millisecond),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1455,8 +1455,7 @@ func (q *fakeQuerier) UpdateTemplateMetaByID(_ context.Context, arg database.Upd
|
||||
tpl.Name = arg.Name
|
||||
tpl.Description = arg.Description
|
||||
tpl.Icon = arg.Icon
|
||||
tpl.MaxTtl = arg.MaxTtl
|
||||
tpl.MinAutostartInterval = arg.MinAutostartInterval
|
||||
tpl.DefaultTtl = arg.DefaultTtl
|
||||
q.templates[idx] = tpl
|
||||
return tpl, nil
|
||||
}
|
||||
@ -2227,10 +2226,6 @@ func (q *fakeQuerier) InsertTemplate(_ context.Context, arg database.InsertTempl
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
||||
if arg.MinAutostartInterval == 0 {
|
||||
arg.MinAutostartInterval = int64(time.Hour)
|
||||
}
|
||||
|
||||
//nolint:gosimple
|
||||
template := database.Template{
|
||||
ID: arg.ID,
|
||||
@ -2241,8 +2236,7 @@ func (q *fakeQuerier) InsertTemplate(_ context.Context, arg database.InsertTempl
|
||||
Provisioner: arg.Provisioner,
|
||||
ActiveVersionID: arg.ActiveVersionID,
|
||||
Description: arg.Description,
|
||||
MaxTtl: arg.MaxTtl,
|
||||
MinAutostartInterval: arg.MinAutostartInterval,
|
||||
DefaultTtl: arg.DefaultTtl,
|
||||
CreatedBy: arg.CreatedBy,
|
||||
UserACL: arg.UserACL,
|
||||
GroupACL: arg.GroupACL,
|
||||
|
5
coderd/database/dump.sql
generated
5
coderd/database/dump.sql
generated
@ -349,14 +349,15 @@ CREATE TABLE templates (
|
||||
provisioner provisioner_type NOT NULL,
|
||||
active_version_id uuid NOT NULL,
|
||||
description character varying(128) DEFAULT ''::character varying NOT NULL,
|
||||
max_ttl bigint DEFAULT '604800000000000'::bigint NOT NULL,
|
||||
min_autostart_interval bigint DEFAULT '3600000000000'::bigint NOT NULL,
|
||||
default_ttl bigint DEFAULT '604800000000000'::bigint NOT NULL,
|
||||
created_by uuid NOT NULL,
|
||||
icon character varying(256) DEFAULT ''::character varying NOT NULL,
|
||||
user_acl jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
group_acl jsonb DEFAULT '{}'::jsonb NOT NULL
|
||||
);
|
||||
|
||||
COMMENT ON COLUMN templates.default_ttl IS 'The default duration for auto-stop for workspaces created from this template.';
|
||||
|
||||
CREATE TABLE user_links (
|
||||
user_id uuid NOT NULL,
|
||||
login_type login_type NOT NULL,
|
||||
|
@ -0,0 +1,5 @@
|
||||
-- add "slug" min_autostart_interval to "templates" table
|
||||
ALTER TABLE "templates" ADD COLUMN "min_autostart_interval" int DEFAULT 0;
|
||||
|
||||
-- rename "default_ttl" to "max_ttl" on "templates" table
|
||||
ALTER TABLE "templates" RENAME COLUMN "default_ttl" TO "max_ttl";
|
@ -0,0 +1,6 @@
|
||||
-- drop "min_autostart_interval" column from "templates" table
|
||||
ALTER TABLE "templates" DROP COLUMN "min_autostart_interval";
|
||||
|
||||
-- rename "max_ttl" to "default_ttl" on "templates" table
|
||||
ALTER TABLE "templates" RENAME COLUMN "max_ttl" TO "default_ttl";
|
||||
COMMENT ON COLUMN templates.default_ttl IS 'The default duration for auto-stop for workspaces created from this template.';
|
@ -583,8 +583,8 @@ type Template struct {
|
||||
Provisioner ProvisionerType `db:"provisioner" json:"provisioner"`
|
||||
ActiveVersionID uuid.UUID `db:"active_version_id" json:"active_version_id"`
|
||||
Description string `db:"description" json:"description"`
|
||||
MaxTtl int64 `db:"max_ttl" json:"max_ttl"`
|
||||
MinAutostartInterval int64 `db:"min_autostart_interval" json:"min_autostart_interval"`
|
||||
// The default duration for auto-stop for workspaces created from this template.
|
||||
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"`
|
||||
|
@ -3019,7 +3019,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, max_ttl, min_autostart_interval, created_by, icon, user_acl, group_acl
|
||||
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl
|
||||
FROM
|
||||
templates
|
||||
WHERE
|
||||
@ -3041,8 +3041,7 @@ func (q *sqlQuerier) GetTemplateByID(ctx context.Context, id uuid.UUID) (Templat
|
||||
&i.Provisioner,
|
||||
&i.ActiveVersionID,
|
||||
&i.Description,
|
||||
&i.MaxTtl,
|
||||
&i.MinAutostartInterval,
|
||||
&i.DefaultTtl,
|
||||
&i.CreatedBy,
|
||||
&i.Icon,
|
||||
&i.UserACL,
|
||||
@ -3053,7 +3052,7 @@ func (q *sqlQuerier) GetTemplateByID(ctx context.Context, id uuid.UUID) (Templat
|
||||
|
||||
const getTemplateByOrganizationAndName = `-- name: GetTemplateByOrganizationAndName :one
|
||||
SELECT
|
||||
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by, icon, user_acl, group_acl
|
||||
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl
|
||||
FROM
|
||||
templates
|
||||
WHERE
|
||||
@ -3083,8 +3082,7 @@ func (q *sqlQuerier) GetTemplateByOrganizationAndName(ctx context.Context, arg G
|
||||
&i.Provisioner,
|
||||
&i.ActiveVersionID,
|
||||
&i.Description,
|
||||
&i.MaxTtl,
|
||||
&i.MinAutostartInterval,
|
||||
&i.DefaultTtl,
|
||||
&i.CreatedBy,
|
||||
&i.Icon,
|
||||
&i.UserACL,
|
||||
@ -3094,7 +3092,7 @@ func (q *sqlQuerier) GetTemplateByOrganizationAndName(ctx context.Context, arg G
|
||||
}
|
||||
|
||||
const getTemplates = `-- name: GetTemplates :many
|
||||
SELECT id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by, icon, user_acl, group_acl 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 FROM templates
|
||||
ORDER BY (name, id) ASC
|
||||
`
|
||||
|
||||
@ -3117,8 +3115,7 @@ func (q *sqlQuerier) GetTemplates(ctx context.Context) ([]Template, error) {
|
||||
&i.Provisioner,
|
||||
&i.ActiveVersionID,
|
||||
&i.Description,
|
||||
&i.MaxTtl,
|
||||
&i.MinAutostartInterval,
|
||||
&i.DefaultTtl,
|
||||
&i.CreatedBy,
|
||||
&i.Icon,
|
||||
&i.UserACL,
|
||||
@ -3139,7 +3136,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, max_ttl, min_autostart_interval, created_by, icon, user_acl, group_acl
|
||||
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl
|
||||
FROM
|
||||
templates
|
||||
WHERE
|
||||
@ -3197,8 +3194,7 @@ func (q *sqlQuerier) GetTemplatesWithFilter(ctx context.Context, arg GetTemplate
|
||||
&i.Provisioner,
|
||||
&i.ActiveVersionID,
|
||||
&i.Description,
|
||||
&i.MaxTtl,
|
||||
&i.MinAutostartInterval,
|
||||
&i.DefaultTtl,
|
||||
&i.CreatedBy,
|
||||
&i.Icon,
|
||||
&i.UserACL,
|
||||
@ -3228,15 +3224,14 @@ INSERT INTO
|
||||
provisioner,
|
||||
active_version_id,
|
||||
description,
|
||||
max_ttl,
|
||||
min_autostart_interval,
|
||||
default_ttl,
|
||||
created_by,
|
||||
icon,
|
||||
user_acl,
|
||||
group_acl
|
||||
)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by, icon, user_acl, group_acl
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl
|
||||
`
|
||||
|
||||
type InsertTemplateParams struct {
|
||||
@ -3248,8 +3243,7 @@ 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"`
|
||||
MaxTtl int64 `db:"max_ttl" json:"max_ttl"`
|
||||
MinAutostartInterval int64 `db:"min_autostart_interval" json:"min_autostart_interval"`
|
||||
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"`
|
||||
@ -3266,8 +3260,7 @@ func (q *sqlQuerier) InsertTemplate(ctx context.Context, arg InsertTemplateParam
|
||||
arg.Provisioner,
|
||||
arg.ActiveVersionID,
|
||||
arg.Description,
|
||||
arg.MaxTtl,
|
||||
arg.MinAutostartInterval,
|
||||
arg.DefaultTtl,
|
||||
arg.CreatedBy,
|
||||
arg.Icon,
|
||||
arg.UserACL,
|
||||
@ -3284,8 +3277,7 @@ func (q *sqlQuerier) InsertTemplate(ctx context.Context, arg InsertTemplateParam
|
||||
&i.Provisioner,
|
||||
&i.ActiveVersionID,
|
||||
&i.Description,
|
||||
&i.MaxTtl,
|
||||
&i.MinAutostartInterval,
|
||||
&i.DefaultTtl,
|
||||
&i.CreatedBy,
|
||||
&i.Icon,
|
||||
&i.UserACL,
|
||||
@ -3303,7 +3295,7 @@ SET
|
||||
WHERE
|
||||
id = $3
|
||||
RETURNING
|
||||
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by, icon, user_acl, group_acl
|
||||
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl
|
||||
`
|
||||
|
||||
type UpdateTemplateACLByIDParams struct {
|
||||
@ -3325,8 +3317,7 @@ func (q *sqlQuerier) UpdateTemplateACLByID(ctx context.Context, arg UpdateTempla
|
||||
&i.Provisioner,
|
||||
&i.ActiveVersionID,
|
||||
&i.Description,
|
||||
&i.MaxTtl,
|
||||
&i.MinAutostartInterval,
|
||||
&i.DefaultTtl,
|
||||
&i.CreatedBy,
|
||||
&i.Icon,
|
||||
&i.UserACL,
|
||||
@ -3383,22 +3374,20 @@ UPDATE
|
||||
SET
|
||||
updated_at = $2,
|
||||
description = $3,
|
||||
max_ttl = $4,
|
||||
min_autostart_interval = $5,
|
||||
name = $6,
|
||||
icon = $7
|
||||
default_ttl = $4,
|
||||
name = $5,
|
||||
icon = $6
|
||||
WHERE
|
||||
id = $1
|
||||
RETURNING
|
||||
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by, icon, user_acl, group_acl
|
||||
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl
|
||||
`
|
||||
|
||||
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"`
|
||||
MaxTtl int64 `db:"max_ttl" json:"max_ttl"`
|
||||
MinAutostartInterval int64 `db:"min_autostart_interval" json:"min_autostart_interval"`
|
||||
DefaultTtl int64 `db:"default_ttl" json:"default_ttl"`
|
||||
Name string `db:"name" json:"name"`
|
||||
Icon string `db:"icon" json:"icon"`
|
||||
}
|
||||
@ -3408,8 +3397,7 @@ func (q *sqlQuerier) UpdateTemplateMetaByID(ctx context.Context, arg UpdateTempl
|
||||
arg.ID,
|
||||
arg.UpdatedAt,
|
||||
arg.Description,
|
||||
arg.MaxTtl,
|
||||
arg.MinAutostartInterval,
|
||||
arg.DefaultTtl,
|
||||
arg.Name,
|
||||
arg.Icon,
|
||||
)
|
||||
@ -3424,8 +3412,7 @@ func (q *sqlQuerier) UpdateTemplateMetaByID(ctx context.Context, arg UpdateTempl
|
||||
&i.Provisioner,
|
||||
&i.ActiveVersionID,
|
||||
&i.Description,
|
||||
&i.MaxTtl,
|
||||
&i.MinAutostartInterval,
|
||||
&i.DefaultTtl,
|
||||
&i.CreatedBy,
|
||||
&i.Icon,
|
||||
&i.UserACL,
|
||||
|
@ -65,15 +65,14 @@ INSERT INTO
|
||||
provisioner,
|
||||
active_version_id,
|
||||
description,
|
||||
max_ttl,
|
||||
min_autostart_interval,
|
||||
default_ttl,
|
||||
created_by,
|
||||
icon,
|
||||
user_acl,
|
||||
group_acl
|
||||
)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING *;
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING *;
|
||||
|
||||
-- name: UpdateTemplateActiveVersionByID :exec
|
||||
UPDATE
|
||||
@ -99,10 +98,9 @@ UPDATE
|
||||
SET
|
||||
updated_at = $2,
|
||||
description = $3,
|
||||
max_ttl = $4,
|
||||
min_autostart_interval = $5,
|
||||
name = $6,
|
||||
icon = $7
|
||||
default_ttl = $4,
|
||||
name = $5,
|
||||
icon = $6
|
||||
WHERE
|
||||
id = $1
|
||||
RETURNING
|
||||
|
@ -22,15 +22,9 @@ import (
|
||||
"github.com/coder/coder/coderd/httpmw"
|
||||
"github.com/coder/coder/coderd/rbac"
|
||||
"github.com/coder/coder/coderd/telemetry"
|
||||
"github.com/coder/coder/coderd/util/ptr"
|
||||
"github.com/coder/coder/codersdk"
|
||||
)
|
||||
|
||||
var (
|
||||
maxTTLDefault = 24 * 7 * time.Hour
|
||||
minAutostartIntervalDefault = time.Hour
|
||||
)
|
||||
|
||||
// Auto-importable templates. These can be auto-imported after the first user
|
||||
// has been created.
|
||||
type AutoImportTemplate string
|
||||
@ -212,35 +206,20 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
|
||||
return
|
||||
}
|
||||
|
||||
maxTTL := maxTTLDefault
|
||||
if createTemplate.MaxTTLMillis != nil {
|
||||
maxTTL = time.Duration(*createTemplate.MaxTTLMillis) * time.Millisecond
|
||||
var ttl time.Duration
|
||||
if createTemplate.DefaultTTLMillis != nil {
|
||||
ttl = time.Duration(*createTemplate.DefaultTTLMillis) * time.Millisecond
|
||||
}
|
||||
if maxTTL < 0 {
|
||||
if ttl < 0 {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Invalid create template request.",
|
||||
Validations: []codersdk.ValidationError{
|
||||
{Field: "max_ttl_ms", Detail: "Must be a positive integer."},
|
||||
{Field: "default_ttl_ms", Detail: "Must be a positive integer."},
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if maxTTL > maxTTLDefault {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Invalid create template request.",
|
||||
Validations: []codersdk.ValidationError{
|
||||
{Field: "max_ttl_ms", Detail: "Cannot be greater than " + maxTTLDefault.String()},
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
minAutostartInterval := minAutostartIntervalDefault
|
||||
if !ptr.NilOrZero(createTemplate.MinAutostartIntervalMillis) {
|
||||
minAutostartInterval = time.Duration(*createTemplate.MinAutostartIntervalMillis) * time.Millisecond
|
||||
}
|
||||
|
||||
var dbTemplate database.Template
|
||||
var template codersdk.Template
|
||||
err = api.Database.InTx(func(tx database.Store) error {
|
||||
@ -254,8 +233,7 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
|
||||
Provisioner: importJob.Provisioner,
|
||||
ActiveVersionID: templateVersion.ID,
|
||||
Description: createTemplate.Description,
|
||||
MaxTtl: int64(maxTTL),
|
||||
MinAutostartInterval: int64(minAutostartInterval),
|
||||
DefaultTtl: int64(ttl),
|
||||
CreatedBy: apiKey.UserID,
|
||||
UserACL: database.TemplateACL{},
|
||||
GroupACL: database.TemplateACL{
|
||||
@ -464,14 +442,8 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
var validErrs []codersdk.ValidationError
|
||||
if req.MaxTTLMillis < 0 {
|
||||
validErrs = append(validErrs, codersdk.ValidationError{Field: "max_ttl_ms", Detail: "Must be a positive integer."})
|
||||
}
|
||||
if req.MinAutostartIntervalMillis < 0 {
|
||||
validErrs = append(validErrs, codersdk.ValidationError{Field: "min_autostart_interval_ms", Detail: "Must be a positive integer."})
|
||||
}
|
||||
if req.MaxTTLMillis > maxTTLDefault.Milliseconds() {
|
||||
validErrs = append(validErrs, codersdk.ValidationError{Field: "max_ttl_ms", Detail: "Cannot be greater than " + maxTTLDefault.String()})
|
||||
if req.DefaultTTLMillis < 0 {
|
||||
validErrs = append(validErrs, codersdk.ValidationError{Field: "default_ttl_ms", Detail: "Must be a positive integer."})
|
||||
}
|
||||
|
||||
if len(validErrs) > 0 {
|
||||
@ -501,8 +473,7 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
|
||||
if req.Name == template.Name &&
|
||||
req.Description == template.Description &&
|
||||
req.Icon == template.Icon &&
|
||||
req.MaxTTLMillis == time.Duration(template.MaxTtl).Milliseconds() &&
|
||||
req.MinAutostartIntervalMillis == time.Duration(template.MinAutostartInterval).Milliseconds() {
|
||||
req.DefaultTTLMillis == time.Duration(template.DefaultTtl).Milliseconds() {
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -510,8 +481,7 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
|
||||
name := req.Name
|
||||
desc := req.Description
|
||||
icon := req.Icon
|
||||
maxTTL := time.Duration(req.MaxTTLMillis) * time.Millisecond
|
||||
minAutostartInterval := time.Duration(req.MinAutostartIntervalMillis) * time.Millisecond
|
||||
maxTTL := time.Duration(req.DefaultTTLMillis) * time.Millisecond
|
||||
|
||||
if name == "" {
|
||||
name = template.Name
|
||||
@ -519,9 +489,6 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
|
||||
if desc == "" {
|
||||
desc = template.Description
|
||||
}
|
||||
if minAutostartInterval == 0 {
|
||||
minAutostartInterval = time.Duration(template.MinAutostartInterval)
|
||||
}
|
||||
|
||||
updated, err = tx.UpdateTemplateMetaByID(ctx, database.UpdateTemplateMetaByIDParams{
|
||||
ID: template.ID,
|
||||
@ -529,8 +496,7 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
|
||||
Name: name,
|
||||
Description: desc,
|
||||
Icon: icon,
|
||||
MaxTtl: int64(maxTTL),
|
||||
MinAutostartInterval: int64(minAutostartInterval),
|
||||
DefaultTtl: int64(maxTTL),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@ -674,8 +640,7 @@ func (api *API) autoImportTemplate(ctx context.Context, opts autoImportTemplateO
|
||||
Provisioner: job.Provisioner,
|
||||
ActiveVersionID: templateVersion.ID,
|
||||
Description: "This template was auto-imported by Coder.",
|
||||
MaxTtl: int64(maxTTLDefault),
|
||||
MinAutostartInterval: int64(minAutostartIntervalDefault),
|
||||
DefaultTtl: 0,
|
||||
CreatedBy: opts.userID,
|
||||
UserACL: database.TemplateACL{},
|
||||
GroupACL: database.TemplateACL{
|
||||
@ -780,8 +745,7 @@ func (api *API) convertTemplate(
|
||||
BuildTimeStats: buildTimeStats,
|
||||
Description: template.Description,
|
||||
Icon: template.Icon,
|
||||
MaxTTLMillis: time.Duration(template.MaxTtl).Milliseconds(),
|
||||
MinAutostartIntervalMillis: time.Duration(template.MinAutostartInterval).Milliseconds(),
|
||||
DefaultTTLMillis: time.Duration(template.DefaultTtl).Milliseconds(),
|
||||
CreatedByID: template.CreatedBy,
|
||||
CreatedByName: createdByName,
|
||||
}
|
||||
|
@ -133,32 +133,12 @@ func TestPostTemplateByOrganization(t *testing.T) {
|
||||
_, err := client.CreateTemplate(ctx, user.OrganizationID, codersdk.CreateTemplateRequest{
|
||||
Name: "testing",
|
||||
VersionID: version.ID,
|
||||
MaxTTLMillis: ptr.Ref(int64(-1)),
|
||||
DefaultTTLMillis: ptr.Ref(int64(-1)),
|
||||
})
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
|
||||
require.Contains(t, err.Error(), "max_ttl_ms: Must be a positive integer")
|
||||
})
|
||||
|
||||
t.Run("MaxTTLTooHigh", 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,
|
||||
MaxTTLMillis: ptr.Ref(365 * 24 * time.Hour.Milliseconds()),
|
||||
})
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
|
||||
require.Contains(t, err.Error(), "max_ttl_ms: Cannot be greater than")
|
||||
require.Contains(t, err.Error(), "default_ttl_ms: Must be a positive integer")
|
||||
})
|
||||
|
||||
t.Run("NoMaxTTL", func(t *testing.T) {
|
||||
@ -173,10 +153,10 @@ func TestPostTemplateByOrganization(t *testing.T) {
|
||||
got, err := client.CreateTemplate(ctx, user.OrganizationID, codersdk.CreateTemplateRequest{
|
||||
Name: "testing",
|
||||
VersionID: version.ID,
|
||||
MaxTTLMillis: ptr.Ref(int64(0)),
|
||||
DefaultTTLMillis: ptr.Ref(int64(0)),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Zero(t, got.MaxTTLMillis)
|
||||
require.Zero(t, got.DefaultTTLMillis)
|
||||
})
|
||||
|
||||
t.Run("Unauthorized", func(t *testing.T) {
|
||||
@ -306,15 +286,13 @@ func TestPatchTemplateMeta(t *testing.T) {
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
|
||||
ctr.Description = "original description"
|
||||
ctr.Icon = "/icons/original-icon.png"
|
||||
ctr.MaxTTLMillis = ptr.Ref(24 * time.Hour.Milliseconds())
|
||||
ctr.MinAutostartIntervalMillis = ptr.Ref(time.Hour.Milliseconds())
|
||||
ctr.DefaultTTLMillis = ptr.Ref(24 * time.Hour.Milliseconds())
|
||||
})
|
||||
req := codersdk.UpdateTemplateMeta{
|
||||
Name: "new-template-name",
|
||||
Description: "lorem ipsum dolor sit amet et cetera",
|
||||
Icon: "/icons/new-icon.png",
|
||||
MaxTTLMillis: 12 * time.Hour.Milliseconds(),
|
||||
MinAutostartIntervalMillis: time.Minute.Milliseconds(),
|
||||
DefaultTTLMillis: 12 * time.Hour.Milliseconds(),
|
||||
}
|
||||
// It is unfortunate we need to sleep, but the test can fail if the
|
||||
// updatedAt is too close together.
|
||||
@ -329,8 +307,7 @@ func TestPatchTemplateMeta(t *testing.T) {
|
||||
assert.Equal(t, req.Name, updated.Name)
|
||||
assert.Equal(t, req.Description, updated.Description)
|
||||
assert.Equal(t, req.Icon, updated.Icon)
|
||||
assert.Equal(t, req.MaxTTLMillis, updated.MaxTTLMillis)
|
||||
assert.Equal(t, req.MinAutostartIntervalMillis, updated.MinAutostartIntervalMillis)
|
||||
assert.Equal(t, req.DefaultTTLMillis, updated.DefaultTTLMillis)
|
||||
|
||||
// Extra paranoid: did it _really_ happen?
|
||||
updated, err = client.Template(ctx, template.ID)
|
||||
@ -339,8 +316,7 @@ func TestPatchTemplateMeta(t *testing.T) {
|
||||
assert.Equal(t, req.Name, updated.Name)
|
||||
assert.Equal(t, req.Description, updated.Description)
|
||||
assert.Equal(t, req.Icon, updated.Icon)
|
||||
assert.Equal(t, req.MaxTTLMillis, updated.MaxTTLMillis)
|
||||
assert.Equal(t, req.MinAutostartIntervalMillis, updated.MinAutostartIntervalMillis)
|
||||
assert.Equal(t, req.DefaultTTLMillis, updated.DefaultTTLMillis)
|
||||
|
||||
require.Len(t, auditor.AuditLogs, 4)
|
||||
assert.Equal(t, database.AuditActionWrite, auditor.AuditLogs[3].Action)
|
||||
@ -353,10 +329,10 @@ func TestPatchTemplateMeta(t *testing.T) {
|
||||
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.MaxTTLMillis = ptr.Ref(24 * time.Hour.Milliseconds())
|
||||
ctr.DefaultTTLMillis = ptr.Ref(24 * time.Hour.Milliseconds())
|
||||
})
|
||||
req := codersdk.UpdateTemplateMeta{
|
||||
MaxTTLMillis: 0,
|
||||
DefaultTTLMillis: 0,
|
||||
}
|
||||
|
||||
// We're too fast! Sleep so we can be sure that updatedAt is greater
|
||||
@ -372,7 +348,7 @@ func TestPatchTemplateMeta(t *testing.T) {
|
||||
updated, err := client.Template(ctx, template.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Greater(t, updated.UpdatedAt, template.UpdatedAt)
|
||||
assert.Equal(t, req.MaxTTLMillis, updated.MaxTTLMillis)
|
||||
assert.Equal(t, req.DefaultTTLMillis, updated.DefaultTTLMillis)
|
||||
})
|
||||
|
||||
t.Run("MaxTTLTooLow", func(t *testing.T) {
|
||||
@ -382,49 +358,23 @@ func TestPatchTemplateMeta(t *testing.T) {
|
||||
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.MaxTTLMillis = ptr.Ref(24 * time.Hour.Milliseconds())
|
||||
ctr.DefaultTTLMillis = ptr.Ref(24 * time.Hour.Milliseconds())
|
||||
})
|
||||
req := codersdk.UpdateTemplateMeta{
|
||||
MaxTTLMillis: -1,
|
||||
DefaultTTLMillis: -1,
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
_, err := client.UpdateTemplateMeta(ctx, template.ID, req)
|
||||
require.ErrorContains(t, err, "max_ttl_ms: Must be a positive integer")
|
||||
require.ErrorContains(t, err, "default_ttl_ms: Must be a positive integer")
|
||||
|
||||
// Ensure no update occurred
|
||||
updated, err := client.Template(ctx, template.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, updated.UpdatedAt, template.UpdatedAt)
|
||||
assert.Equal(t, updated.MaxTTLMillis, template.MaxTTLMillis)
|
||||
})
|
||||
|
||||
t.Run("MaxTTLTooHigh", 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.MaxTTLMillis = ptr.Ref(24 * time.Hour.Milliseconds())
|
||||
})
|
||||
req := codersdk.UpdateTemplateMeta{
|
||||
MaxTTLMillis: 365 * 24 * time.Hour.Milliseconds(),
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
_, err := client.UpdateTemplateMeta(ctx, template.ID, req)
|
||||
require.ErrorContains(t, err, "max_ttl_ms: Cannot be greater than")
|
||||
|
||||
// Ensure no update occurred
|
||||
updated, err := client.Template(ctx, template.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, updated.UpdatedAt, template.UpdatedAt)
|
||||
assert.Equal(t, updated.MaxTTLMillis, template.MaxTTLMillis)
|
||||
assert.Equal(t, updated.DefaultTTLMillis, template.DefaultTTLMillis)
|
||||
})
|
||||
|
||||
t.Run("NotModified", func(t *testing.T) {
|
||||
@ -436,8 +386,7 @@ func TestPatchTemplateMeta(t *testing.T) {
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
|
||||
ctr.Description = "original description"
|
||||
ctr.Icon = "/icons/original-icon.png"
|
||||
ctr.MaxTTLMillis = ptr.Ref(24 * time.Hour.Milliseconds())
|
||||
ctr.MinAutostartIntervalMillis = ptr.Ref(time.Hour.Milliseconds())
|
||||
ctr.DefaultTTLMillis = ptr.Ref(24 * time.Hour.Milliseconds())
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
@ -447,8 +396,7 @@ func TestPatchTemplateMeta(t *testing.T) {
|
||||
Name: template.Name,
|
||||
Description: template.Description,
|
||||
Icon: template.Icon,
|
||||
MaxTTLMillis: template.MaxTTLMillis,
|
||||
MinAutostartIntervalMillis: template.MinAutostartIntervalMillis,
|
||||
DefaultTTLMillis: template.DefaultTTLMillis,
|
||||
}
|
||||
_, err := client.UpdateTemplateMeta(ctx, template.ID, req)
|
||||
require.ErrorContains(t, err, "not modified")
|
||||
@ -458,8 +406,7 @@ func TestPatchTemplateMeta(t *testing.T) {
|
||||
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.MaxTTLMillis, updated.MaxTTLMillis)
|
||||
assert.Equal(t, template.MinAutostartIntervalMillis, updated.MinAutostartIntervalMillis)
|
||||
assert.Equal(t, template.DefaultTTLMillis, updated.DefaultTTLMillis)
|
||||
})
|
||||
|
||||
t.Run("Invalid", func(t *testing.T) {
|
||||
@ -470,24 +417,21 @@ func TestPatchTemplateMeta(t *testing.T) {
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
|
||||
ctr.Description = "original description"
|
||||
ctr.MaxTTLMillis = ptr.Ref(24 * time.Hour.Milliseconds())
|
||||
ctr.MinAutostartIntervalMillis = ptr.Ref(time.Hour.Milliseconds())
|
||||
ctr.DefaultTTLMillis = ptr.Ref(24 * time.Hour.Milliseconds())
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
req := codersdk.UpdateTemplateMeta{
|
||||
MaxTTLMillis: -int64(time.Hour),
|
||||
MinAutostartIntervalMillis: -int64(time.Hour),
|
||||
DefaultTTLMillis: -int64(time.Hour),
|
||||
}
|
||||
_, err := client.UpdateTemplateMeta(ctx, template.ID, req)
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Contains(t, apiErr.Message, "Invalid request")
|
||||
require.Len(t, apiErr.Validations, 2)
|
||||
assert.Equal(t, apiErr.Validations[0].Field, "max_ttl_ms")
|
||||
assert.Equal(t, apiErr.Validations[1].Field, "min_autostart_interval_ms")
|
||||
require.Len(t, apiErr.Validations, 1)
|
||||
assert.Equal(t, apiErr.Validations[0].Field, "default_ttl_ms")
|
||||
|
||||
updated, err := client.Template(ctx, template.ID)
|
||||
require.NoError(t, err)
|
||||
@ -495,8 +439,7 @@ func TestPatchTemplateMeta(t *testing.T) {
|
||||
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.MaxTTLMillis, updated.MaxTTLMillis)
|
||||
assert.Equal(t, template.MinAutostartIntervalMillis, updated.MinAutostartIntervalMillis)
|
||||
assert.Equal(t, template.DefaultTTLMillis, updated.DefaultTTLMillis)
|
||||
})
|
||||
|
||||
t.Run("RemoveIcon", func(t *testing.T) {
|
||||
|
@ -41,7 +41,6 @@ var (
|
||||
errTTLMax = xerrors.New("time until shutdown must be less than 7 days")
|
||||
errDeadlineTooSoon = xerrors.New("new deadline must be at least 30 minutes in the future")
|
||||
errDeadlineBeforeStart = xerrors.New("new deadline must be before workspace start time")
|
||||
errDeadlineOverTemplateMax = xerrors.New("new deadline is greater than template allows")
|
||||
)
|
||||
|
||||
func (api *API) workspace(rw http.ResponseWriter, r *http.Request) {
|
||||
@ -333,7 +332,7 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
|
||||
return
|
||||
}
|
||||
|
||||
dbAutostartSchedule, err := validWorkspaceSchedule(createWorkspace.AutostartSchedule, time.Duration(template.MinAutostartInterval))
|
||||
dbAutostartSchedule, err := validWorkspaceSchedule(createWorkspace.AutostartSchedule)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Invalid Autostart Schedule.",
|
||||
@ -342,7 +341,7 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
|
||||
return
|
||||
}
|
||||
|
||||
dbTTL, err := validWorkspaceTTLMillis(createWorkspace.TTLMillis, time.Duration(template.MaxTtl))
|
||||
dbTTL, err := validWorkspaceTTLMillis(createWorkspace.TTLMillis, template.DefaultTtl)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Invalid Workspace Time to Shutdown.",
|
||||
@ -666,16 +665,7 @@ func (api *API) putWorkspaceAutostart(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
template, err := api.Database.GetTemplateByID(ctx, workspace.TemplateID)
|
||||
if err != nil {
|
||||
api.Logger.Error(ctx, "fetch workspace template", slog.F("workspace_id", workspace.ID), slog.F("template_id", workspace.TemplateID), slog.Error(err))
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Error fetching workspace template.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
dbSched, err := validWorkspaceSchedule(req.Schedule, time.Duration(template.MinAutostartInterval))
|
||||
dbSched, err := validWorkspaceSchedule(req.Schedule)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Invalid autostart schedule.",
|
||||
@ -739,7 +729,7 @@ func (api *API) putWorkspaceTTL(rw http.ResponseWriter, r *http.Request) {
|
||||
return xerrors.Errorf("fetch workspace template: %w", err)
|
||||
}
|
||||
|
||||
dbTTL, err = validWorkspaceTTLMillis(req.TTLMillis, time.Duration(template.MaxTtl))
|
||||
dbTTL, err = validWorkspaceTTLMillis(req.TTLMillis, template.DefaultTtl)
|
||||
if err != nil {
|
||||
return codersdk.ValidationError{Field: "ttl_ms", Detail: err.Error()}
|
||||
}
|
||||
@ -793,13 +783,6 @@ func (api *API) putExtendWorkspace(rw http.ResponseWriter, r *http.Request) {
|
||||
resp := codersdk.Response{}
|
||||
|
||||
err := api.Database.InTx(func(s database.Store) error {
|
||||
template, err := s.GetTemplateByID(ctx, workspace.TemplateID)
|
||||
if err != nil {
|
||||
code = http.StatusInternalServerError
|
||||
resp.Message = "Error fetching workspace template!"
|
||||
return xerrors.Errorf("get workspace template: %w", err)
|
||||
}
|
||||
|
||||
build, err := s.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID)
|
||||
if err != nil {
|
||||
code = http.StatusInternalServerError
|
||||
@ -833,7 +816,7 @@ func (api *API) putExtendWorkspace(rw http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
newDeadline := req.Deadline.UTC()
|
||||
if err := validWorkspaceDeadline(job.CompletedAt.Time, newDeadline, time.Duration(template.MaxTtl)); err != nil {
|
||||
if err := validWorkspaceDeadline(job.CompletedAt.Time, newDeadline); err != nil {
|
||||
// NOTE(Cian): Putting the error in the Message field on request from the FE folks.
|
||||
// Normally, we would put the validation error in Validations, but this endpoint is
|
||||
// not tied to a form or specific named user input on the FE.
|
||||
@ -1104,11 +1087,18 @@ func convertWorkspaceTTLMillis(i sql.NullInt64) *int64 {
|
||||
return &millis
|
||||
}
|
||||
|
||||
func validWorkspaceTTLMillis(millis *int64, max time.Duration) (sql.NullInt64, error) {
|
||||
func validWorkspaceTTLMillis(millis *int64, def int64) (sql.NullInt64, error) {
|
||||
if ptr.NilOrZero(millis) {
|
||||
if def == 0 {
|
||||
return sql.NullInt64{}, nil
|
||||
}
|
||||
|
||||
return sql.NullInt64{
|
||||
Int64: def,
|
||||
Valid: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
dur := time.Duration(*millis) * time.Millisecond
|
||||
truncated := dur.Truncate(time.Minute)
|
||||
if truncated < ttlMin {
|
||||
@ -1119,18 +1109,13 @@ func validWorkspaceTTLMillis(millis *int64, max time.Duration) (sql.NullInt64, e
|
||||
return sql.NullInt64{}, errTTLMax
|
||||
}
|
||||
|
||||
// template level
|
||||
if max > 0 && truncated > max {
|
||||
return sql.NullInt64{}, xerrors.Errorf("time until shutdown must be below template maximum %s", max.String())
|
||||
}
|
||||
|
||||
return sql.NullInt64{
|
||||
Valid: true,
|
||||
Int64: int64(truncated),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func validWorkspaceDeadline(startedAt, newDeadline time.Time, max time.Duration) error {
|
||||
func validWorkspaceDeadline(startedAt, newDeadline time.Time) error {
|
||||
soon := time.Now().Add(29 * time.Minute)
|
||||
if newDeadline.Before(soon) {
|
||||
return errDeadlineTooSoon
|
||||
@ -1141,28 +1126,19 @@ func validWorkspaceDeadline(startedAt, newDeadline time.Time, max time.Duration)
|
||||
return errDeadlineBeforeStart
|
||||
}
|
||||
|
||||
delta := newDeadline.Sub(startedAt)
|
||||
if delta > max {
|
||||
return errDeadlineOverTemplateMax
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validWorkspaceSchedule(s *string, min time.Duration) (sql.NullString, error) {
|
||||
func validWorkspaceSchedule(s *string) (sql.NullString, error) {
|
||||
if ptr.NilOrEmpty(s) {
|
||||
return sql.NullString{}, nil
|
||||
}
|
||||
|
||||
sched, err := schedule.Weekly(*s)
|
||||
_, err := schedule.Weekly(*s)
|
||||
if err != nil {
|
||||
return sql.NullString{}, err
|
||||
}
|
||||
|
||||
if schedMin := sched.Min(); schedMin < min {
|
||||
return sql.NullString{}, xerrors.Errorf("Minimum autostart interval %s below template minimum %s", schedMin, min)
|
||||
}
|
||||
|
||||
return sql.NullString{
|
||||
Valid: true,
|
||||
String: *s,
|
||||
|
@ -239,10 +239,10 @@ func TestPostWorkspacesByOrganization(t *testing.T) {
|
||||
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.MaxTTLMillis = ptr.Ref(int64(0))
|
||||
ctr.DefaultTTLMillis = ptr.Ref(int64(0))
|
||||
})
|
||||
// Given: the template has no max TTL set
|
||||
require.Zero(t, template.MaxTTLMillis)
|
||||
require.Zero(t, template.DefaultTTLMillis)
|
||||
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||
|
||||
// When: we create a workspace with autostop not enabled
|
||||
@ -260,15 +260,15 @@ func TestPostWorkspacesByOrganization(t *testing.T) {
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
templateTTL := 24 * time.Hour.Milliseconds()
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
|
||||
ctr.MaxTTLMillis = ptr.Ref(templateTTL)
|
||||
ctr.DefaultTTLMillis = ptr.Ref(templateTTL)
|
||||
})
|
||||
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
|
||||
cwr.TTLMillis = nil // ensure that no default TTL is set
|
||||
})
|
||||
// TTL should be set by the template
|
||||
require.Equal(t, template.MaxTTLMillis, templateTTL)
|
||||
require.Equal(t, template.MaxTTLMillis, template.MaxTTLMillis, workspace.TTLMillis)
|
||||
require.Equal(t, template.DefaultTTLMillis, templateTTL)
|
||||
require.Equal(t, template.DefaultTTLMillis, template.DefaultTTLMillis, workspace.TTLMillis)
|
||||
})
|
||||
|
||||
t.Run("InvalidTTL", func(t *testing.T) {
|
||||
@ -298,58 +298,38 @@ func TestPostWorkspacesByOrganization(t *testing.T) {
|
||||
require.Equal(t, apiErr.Validations[0].Field, "ttl_ms")
|
||||
require.Equal(t, "time until shutdown must be at least one minute", apiErr.Validations[0].Detail)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("AboveMax", func(t *testing.T) {
|
||||
t.Run("TemplateDefaultTTL", 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)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
exp := 24 * time.Hour.Milliseconds()
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
|
||||
ctr.DefaultTTLMillis = &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",
|
||||
TTLMillis: ptr.Ref(template.MaxTTLMillis + time.Minute.Milliseconds()),
|
||||
}
|
||||
_, err := client.CreateWorkspace(ctx, template.OrganizationID, codersdk.Me, req)
|
||||
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.Equal(t, "time until shutdown must be less than 7 days", apiErr.Validations[0].Detail)
|
||||
})
|
||||
})
|
||||
ws, err := client.CreateWorkspace(ctx, template.OrganizationID, codersdk.Me, req)
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, exp, *ws.TTLMillis)
|
||||
|
||||
t.Run("InvalidAutostart", 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)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
req := codersdk.CreateWorkspaceRequest{
|
||||
TemplateID: template.ID,
|
||||
Name: "testing",
|
||||
AutostartSchedule: ptr.Ref("CRON_TZ=US/Central * * * * *"),
|
||||
}
|
||||
_, err := client.CreateWorkspace(ctx, template.OrganizationID, codersdk.Me, req)
|
||||
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, "schedule")
|
||||
require.Equal(t, apiErr.Validations[0].Detail, "Minimum autostart interval 1m0s below template minimum 1h0m0s")
|
||||
// TTL provided should override template default
|
||||
req.Name = "testing2"
|
||||
exp = 1 * time.Hour.Milliseconds()
|
||||
req.TTLMillis = &exp
|
||||
ws, err = client.CreateWorkspace(ctx, template.OrganizationID, codersdk.Me, req)
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, exp, *ws.TTLMillis)
|
||||
})
|
||||
}
|
||||
|
||||
@ -1187,23 +1167,6 @@ func TestWorkspaceUpdateTTL(t *testing.T) {
|
||||
ttlMillis: ptr.Ref((24*7*time.Hour + time.Minute).Milliseconds()),
|
||||
expectedError: "time until shutdown must be less than 7 days",
|
||||
},
|
||||
{
|
||||
name: "above template maximum ttl",
|
||||
ttlMillis: ptr.Ref((12 * time.Hour).Milliseconds()),
|
||||
expectedError: "ttl_ms: time until shutdown must be below template maximum 8h0m0s",
|
||||
modifyTemplate: func(ctr *codersdk.CreateTemplateRequest) { ctr.MaxTTLMillis = ptr.Ref((8 * time.Hour).Milliseconds()) },
|
||||
},
|
||||
{
|
||||
name: "no template maximum ttl",
|
||||
ttlMillis: ptr.Ref((7 * 24 * time.Hour).Milliseconds()),
|
||||
modifyTemplate: func(ctr *codersdk.CreateTemplateRequest) { ctr.MaxTTLMillis = ptr.Ref(int64(0)) },
|
||||
},
|
||||
{
|
||||
name: "above maximum ttl even with no template max",
|
||||
ttlMillis: ptr.Ref((365 * 24 * time.Hour).Milliseconds()),
|
||||
expectedError: "ttl_ms: time until shutdown must be less than 7 days",
|
||||
modifyTemplate: func(ctr *codersdk.CreateTemplateRequest) { ctr.MaxTTLMillis = ptr.Ref(int64(0)) },
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
@ -1322,14 +1285,6 @@ func TestWorkspaceExtend(t *testing.T) {
|
||||
})
|
||||
require.ErrorContains(t, err, "unexpected status code 400: Cannot extend workspace: new deadline must be at least 30 minutes in the future", "setting a deadline less than 30 minutes in the future should fail")
|
||||
|
||||
// And with a deadline greater than the template max_ttl should also fail
|
||||
deadlineExceedsMaxTTL := time.Now().Add(time.Duration(template.MaxTTLMillis) * time.Millisecond).Add(time.Minute)
|
||||
err = client.PutExtendWorkspace(ctx, workspace.ID, codersdk.PutExtendWorkspaceRequest{
|
||||
Deadline: deadlineExceedsMaxTTL,
|
||||
})
|
||||
|
||||
require.ErrorContains(t, err, "unexpected status code 400: Cannot extend workspace: new deadline is greater than template allows", "setting a deadline greater than that allowed by the template should fail")
|
||||
|
||||
// Updating with a deadline 30 minutes in the future should succeed
|
||||
deadlineJustSoonEnough := time.Now().Add(30 * time.Minute)
|
||||
err = client.PutExtendWorkspace(ctx, workspace.ID, codersdk.PutExtendWorkspaceRequest{
|
||||
|
@ -66,14 +66,9 @@ type CreateTemplateRequest struct {
|
||||
VersionID uuid.UUID `json:"template_version_id" validate:"required"`
|
||||
ParameterValues []CreateParameterRequest `json:"parameter_values,omitempty"`
|
||||
|
||||
// MaxTTLMillis allows optionally specifying the maximum allowable TTL
|
||||
// DefaultTTLMillis allows optionally specifying the default TTL
|
||||
// for all workspaces created from this template.
|
||||
MaxTTLMillis *int64 `json:"max_ttl_ms,omitempty"`
|
||||
|
||||
// MinAutostartIntervalMillis allows optionally specifying the minimum
|
||||
// allowable duration between autostarts for all workspaces created from
|
||||
// this template.
|
||||
MinAutostartIntervalMillis *int64 `json:"min_autostart_interval_ms,omitempty"`
|
||||
DefaultTTLMillis *int64 `json:"default_ttl_ms,omitempty"`
|
||||
}
|
||||
|
||||
// CreateWorkspaceRequest provides options for creating a new workspace.
|
||||
|
@ -27,8 +27,7 @@ type Template struct {
|
||||
BuildTimeStats TemplateBuildTimeStats `json:"build_time_stats"`
|
||||
Description string `json:"description"`
|
||||
Icon string `json:"icon"`
|
||||
MaxTTLMillis int64 `json:"max_ttl_ms"`
|
||||
MinAutostartIntervalMillis int64 `json:"min_autostart_interval_ms"`
|
||||
DefaultTTLMillis int64 `json:"default_ttl_ms"`
|
||||
CreatedByID uuid.UUID `json:"created_by_id"`
|
||||
CreatedByName string `json:"created_by_name"`
|
||||
}
|
||||
@ -75,8 +74,7 @@ type UpdateTemplateMeta struct {
|
||||
Name string `json:"name,omitempty" validate:"omitempty,username"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
MaxTTLMillis int64 `json:"max_ttl_ms,omitempty"`
|
||||
MinAutostartIntervalMillis int64 `json:"min_autostart_interval_ms,omitempty"`
|
||||
DefaultTTLMillis int64 `json:"default_ttl_ms,omitempty"`
|
||||
}
|
||||
|
||||
// Template returns a single template.
|
||||
|
@ -250,8 +250,7 @@ func Test_diff(t *testing.T) {
|
||||
Name: "rust",
|
||||
Provisioner: database.ProvisionerTypeTerraform,
|
||||
ActiveVersionID: uuid.UUID{3},
|
||||
MaxTtl: int64(time.Hour),
|
||||
MinAutostartInterval: int64(time.Minute),
|
||||
DefaultTtl: int64(time.Hour),
|
||||
CreatedBy: uuid.UUID{4},
|
||||
},
|
||||
exp: audit.Map{
|
||||
@ -259,8 +258,7 @@ func Test_diff(t *testing.T) {
|
||||
"name": audit.OldNew{Old: "", New: "rust"},
|
||||
"provisioner": audit.OldNew{Old: database.ProvisionerType(""), New: database.ProvisionerTypeTerraform},
|
||||
"active_version_id": audit.OldNew{Old: "", New: uuid.UUID{3}.String()},
|
||||
"max_ttl": audit.OldNew{Old: int64(0), New: int64(time.Hour)},
|
||||
"min_autostart_interval": audit.OldNew{Old: int64(0), New: int64(time.Minute)},
|
||||
"default_ttl": audit.OldNew{Old: int64(0), New: int64(time.Hour)},
|
||||
"created_by": audit.OldNew{Old: "", New: uuid.UUID{4}.String()},
|
||||
},
|
||||
},
|
||||
|
@ -58,7 +58,7 @@ var AuditableResources = auditMap(map[any]map[string]Action{
|
||||
"active_version_id": ActionTrack,
|
||||
"description": ActionTrack,
|
||||
"icon": ActionTrack,
|
||||
"max_ttl": ActionTrack,
|
||||
"default_ttl": ActionTrack,
|
||||
"min_autostart_interval": ActionTrack,
|
||||
"created_by": ActionTrack,
|
||||
"is_private": ActionTrack,
|
||||
|
@ -179,8 +179,7 @@ export interface CreateTemplateRequest {
|
||||
readonly icon?: string
|
||||
readonly template_version_id: string
|
||||
readonly parameter_values?: CreateParameterRequest[]
|
||||
readonly max_ttl_ms?: number
|
||||
readonly min_autostart_interval_ms?: number
|
||||
readonly default_ttl_ms?: number
|
||||
}
|
||||
|
||||
// From codersdk/templateversions.go
|
||||
@ -620,8 +619,7 @@ export interface Template {
|
||||
readonly build_time_stats: TemplateBuildTimeStats
|
||||
readonly description: string
|
||||
readonly icon: string
|
||||
readonly max_ttl_ms: number
|
||||
readonly min_autostart_interval_ms: number
|
||||
readonly default_ttl_ms: number
|
||||
readonly created_by_id: string
|
||||
readonly created_by_name: string
|
||||
}
|
||||
@ -700,8 +698,7 @@ export interface UpdateTemplateMeta {
|
||||
readonly name?: string
|
||||
readonly description?: string
|
||||
readonly icon?: string
|
||||
readonly max_ttl_ms?: number
|
||||
readonly min_autostart_interval_ms?: number
|
||||
readonly default_ttl_ms?: number
|
||||
}
|
||||
|
||||
// From codersdk/users.go
|
||||
|
@ -19,7 +19,7 @@ import * as Yup from "yup"
|
||||
export const Language = {
|
||||
nameLabel: "Name",
|
||||
descriptionLabel: "Description",
|
||||
maxTtlLabel: "Auto-stop limit",
|
||||
defaultTtlLabel: "Auto-stop default",
|
||||
iconLabel: "Icon",
|
||||
formAriaLabel: "Template settings form",
|
||||
selectEmoji: "Select emoji",
|
||||
@ -28,7 +28,7 @@ export const Language = {
|
||||
descriptionMaxError:
|
||||
"Please enter a description that is less than or equal to 128 characters.",
|
||||
ttlHelperText: (ttl: number): string =>
|
||||
`Workspaces created from this template may not remain running longer than ${ttl} hours.`,
|
||||
`Workspaces created from this template will default to stopping after ${ttl} hours.`,
|
||||
}
|
||||
|
||||
const MAX_DESCRIPTION_CHAR_LIMIT = 128
|
||||
@ -41,7 +41,7 @@ export const validationSchema = Yup.object({
|
||||
MAX_DESCRIPTION_CHAR_LIMIT,
|
||||
Language.descriptionMaxError,
|
||||
),
|
||||
max_ttl_ms: Yup.number()
|
||||
default_ttl_ms: Yup.number()
|
||||
.integer()
|
||||
.min(0)
|
||||
.max(24 * MAX_TTL_DAYS /* 7 days in hours */, Language.ttlMaxError),
|
||||
@ -72,7 +72,7 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
|
||||
name: template.name,
|
||||
description: template.description,
|
||||
// on display, convert from ms => hours
|
||||
max_ttl_ms: template.max_ttl_ms / MS_HOUR_CONVERSION,
|
||||
default_ttl_ms: template.default_ttl_ms / MS_HOUR_CONVERSION,
|
||||
icon: template.icon,
|
||||
},
|
||||
validationSchema,
|
||||
@ -80,8 +80,8 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
|
||||
// on submit, convert from hours => ms
|
||||
onSubmit({
|
||||
...formData,
|
||||
max_ttl_ms: formData.max_ttl_ms
|
||||
? formData.max_ttl_ms * MS_HOUR_CONVERSION
|
||||
default_ttl_ms: formData.default_ttl_ms
|
||||
? formData.default_ttl_ms * MS_HOUR_CONVERSION
|
||||
: undefined,
|
||||
})
|
||||
},
|
||||
@ -176,20 +176,20 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
|
||||
</div>
|
||||
|
||||
<TextField
|
||||
{...getFieldHelpers("max_ttl_ms")}
|
||||
{...getFieldHelpers("default_ttl_ms")}
|
||||
disabled={isSubmitting}
|
||||
fullWidth
|
||||
inputProps={{ min: 0, step: 1 }}
|
||||
label={Language.maxTtlLabel}
|
||||
label={Language.defaultTtlLabel}
|
||||
variant="outlined"
|
||||
type="number"
|
||||
/>
|
||||
{/* If a value for max_ttl_ms has been entered and
|
||||
{/* If a value for default_ttl_ms has been entered and
|
||||
there are no validation errors for that field, display helper text.
|
||||
We do not use the MUI helper-text prop because it overrides the validation error */}
|
||||
{form.values.max_ttl_ms && !form.errors.max_ttl_ms && (
|
||||
{form.values.default_ttl_ms && !form.errors.default_ttl_ms && (
|
||||
<Typography variant="subtitle2">
|
||||
{Language.ttlHelperText(form.values.max_ttl_ms)}
|
||||
{Language.ttlHelperText(form.values.default_ttl_ms)}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
|
@ -26,15 +26,15 @@ const validFormValues = {
|
||||
name: "Name",
|
||||
description: "A description",
|
||||
icon: "A string",
|
||||
max_ttl_ms: 1,
|
||||
default_ttl_ms: 1,
|
||||
}
|
||||
|
||||
const fillAndSubmitForm = async ({
|
||||
name,
|
||||
description,
|
||||
max_ttl_ms,
|
||||
default_ttl_ms,
|
||||
icon,
|
||||
}: Omit<Required<UpdateTemplateMeta>, "min_autostart_interval_ms">) => {
|
||||
}: Required<UpdateTemplateMeta>) => {
|
||||
const nameField = await screen.findByLabelText(FormLanguage.nameLabel)
|
||||
await userEvent.clear(nameField)
|
||||
await userEvent.type(nameField, name)
|
||||
@ -49,9 +49,9 @@ const fillAndSubmitForm = async ({
|
||||
await userEvent.clear(iconField)
|
||||
await userEvent.type(iconField, icon)
|
||||
|
||||
const maxTtlField = await screen.findByLabelText(FormLanguage.maxTtlLabel)
|
||||
const maxTtlField = await screen.findByLabelText(FormLanguage.defaultTtlLabel)
|
||||
await userEvent.clear(maxTtlField)
|
||||
await userEvent.type(maxTtlField, max_ttl_ms.toString())
|
||||
await userEvent.type(maxTtlField, default_ttl_ms.toString())
|
||||
|
||||
const submitButton = await screen.findByText(
|
||||
FooterFormLanguage.defaultSubmitLabel,
|
||||
@ -87,7 +87,7 @@ describe("TemplateSettingsPage", () => {
|
||||
})
|
||||
|
||||
await fillAndSubmitForm(validFormValues)
|
||||
expect(screen.getByDisplayValue(1)).toBeInTheDocument() // the max_ttl_ms
|
||||
expect(screen.getByDisplayValue(1)).toBeInTheDocument() // the default_ttl_ms
|
||||
await waitFor(() => expect(API.updateTemplateMeta).toBeCalledTimes(1))
|
||||
|
||||
await waitFor(() =>
|
||||
@ -95,7 +95,7 @@ describe("TemplateSettingsPage", () => {
|
||||
"test-template",
|
||||
expect.objectContaining({
|
||||
...validFormValues,
|
||||
max_ttl_ms: 3600000, // the max_ttl_ms to ms
|
||||
default_ttl_ms: 3600000, // the default_ttl_ms to ms
|
||||
}),
|
||||
),
|
||||
)
|
||||
@ -104,7 +104,7 @@ describe("TemplateSettingsPage", () => {
|
||||
it("allows a ttl of 7 days", () => {
|
||||
const values: UpdateTemplateMeta = {
|
||||
...validFormValues,
|
||||
max_ttl_ms: 24 * 7,
|
||||
default_ttl_ms: 24 * 7,
|
||||
}
|
||||
const validate = () => validationSchema.validateSync(values)
|
||||
expect(validate).not.toThrowError()
|
||||
@ -113,7 +113,7 @@ describe("TemplateSettingsPage", () => {
|
||||
it("allows ttl of 0", () => {
|
||||
const values: UpdateTemplateMeta = {
|
||||
...validFormValues,
|
||||
max_ttl_ms: 0,
|
||||
default_ttl_ms: 0,
|
||||
}
|
||||
const validate = () => validationSchema.validateSync(values)
|
||||
expect(validate).not.toThrowError()
|
||||
@ -122,7 +122,7 @@ describe("TemplateSettingsPage", () => {
|
||||
it("disallows a ttl of 7 days + 1 hour", () => {
|
||||
const values: UpdateTemplateMeta = {
|
||||
...validFormValues,
|
||||
max_ttl_ms: 24 * 7 + 1,
|
||||
default_ttl_ms: 24 * 7 + 1,
|
||||
}
|
||||
const validate = () => validationSchema.validateSync(values)
|
||||
expect(validate).toThrowError(FormLanguage.ttlMaxError)
|
||||
|
@ -198,8 +198,7 @@ export const MockTemplate: TypesGen.Template = {
|
||||
delete_ms: 3000,
|
||||
},
|
||||
description: "This is a test description.",
|
||||
max_ttl_ms: 24 * 60 * 60 * 1000,
|
||||
min_autostart_interval_ms: 60 * 60 * 1000,
|
||||
default_ttl_ms: 24 * 60 * 60 * 1000,
|
||||
created_by_id: "test-creator-id",
|
||||
created_by_name: "test_creator",
|
||||
icon: "/icon/code.svg",
|
||||
|
@ -52,7 +52,7 @@ describe("maxDeadline", () => {
|
||||
it("should be never be greater than global max deadline", () => {
|
||||
const template: Template = {
|
||||
...Mocks.MockTemplate,
|
||||
max_ttl_ms: 25 * 60 * 60 * 1000,
|
||||
default_ttl_ms: 25 * 60 * 60 * 1000,
|
||||
}
|
||||
|
||||
// Then: deadlineMinusDisabled should be falsy
|
||||
@ -65,7 +65,7 @@ describe("maxDeadline", () => {
|
||||
it("should be never be greater than global max deadline", () => {
|
||||
const template: Template = {
|
||||
...Mocks.MockTemplate,
|
||||
max_ttl_ms: 4 * 60 * 60 * 1000,
|
||||
default_ttl_ms: 4 * 60 * 60 * 1000,
|
||||
}
|
||||
|
||||
// Then: deadlineMinusDisabled should be falsy
|
||||
@ -95,7 +95,7 @@ describe("canExtendDeadline", () => {
|
||||
|
||||
it("should be falsy if the deadline is more than the template max_ttl", () => {
|
||||
const tooFarAhead = dayjs().add(
|
||||
dayjs.duration(Mocks.MockTemplate.max_ttl_ms, "milliseconds"),
|
||||
dayjs.duration(Mocks.MockTemplate.default_ttl_ms, "milliseconds"),
|
||||
)
|
||||
expect(
|
||||
canExtendDeadline(tooFarAhead, Mocks.MockWorkspace, Mocks.MockTemplate),
|
||||
@ -104,7 +104,7 @@ describe("canExtendDeadline", () => {
|
||||
|
||||
it("should be truth if the deadline is within the template max_ttl", () => {
|
||||
const okDeadline = dayjs().add(
|
||||
dayjs.duration(Mocks.MockTemplate.max_ttl_ms / 2, "milliseconds"),
|
||||
dayjs.duration(Mocks.MockTemplate.default_ttl_ms / 2, "milliseconds"),
|
||||
)
|
||||
expect(
|
||||
canExtendDeadline(okDeadline, Mocks.MockWorkspace, Mocks.MockTemplate),
|
||||
|
@ -139,7 +139,7 @@ export function getMaxDeadline(
|
||||
}
|
||||
const startedAt = dayjs(ws.latest_build.updated_at)
|
||||
const maxTemplateDeadline = startedAt.add(
|
||||
dayjs.duration(tpl.max_ttl_ms, "milliseconds"),
|
||||
dayjs.duration(tpl.default_ttl_ms, "milliseconds"),
|
||||
)
|
||||
const maxGlobalDeadline = startedAt.add(deadlineExtensionMax)
|
||||
return dayjs.min(maxTemplateDeadline, maxGlobalDeadline)
|
||||
|
Reference in New Issue
Block a user