diff --git a/cli/templatecreate.go b/cli/templatecreate.go
index 823a3cd1e45a4..8b3cd75198ec6 100644
--- a/cli/templatecreate.go
+++ b/cli/templatecreate.go
@@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"io"
+ "net/http"
"os"
"path/filepath"
"strings"
@@ -29,6 +30,8 @@ func (r *RootCmd) templateCreate() *clibase.Cmd {
variablesFile string
variables []string
defaultTTL time.Duration
+ failureTTL time.Duration
+ inactivityTTL time.Duration
uploadFlags templateUploadFlags
)
@@ -41,6 +44,30 @@ func (r *RootCmd) templateCreate() *clibase.Cmd {
r.InitClient(client),
),
Handler: func(inv *clibase.Invocation) error {
+ if failureTTL != 0 || inactivityTTL != 0 {
+ // This call can be removed when workspace_actions is no longer experimental
+ experiments, exErr := client.Experiments(inv.Context())
+ if exErr != nil {
+ return xerrors.Errorf("get experiments: %w", exErr)
+ }
+
+ if !experiments.Enabled(codersdk.ExperimentWorkspaceActions) {
+ return xerrors.Errorf("--failure-ttl and --inactivityTTL are experimental features. Use the workspace_actions CODER_EXPERIMENTS flag to set these configuration values.")
+ }
+
+ entitlements, err := client.Entitlements(inv.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 --failure-ttl or --inactivityTTL")
+ } 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 --failure-ttl or --inactivityTTL")
+ }
+ }
+
organization, err := CurrentOrganization(inv, client)
if err != nil {
return err
@@ -96,9 +123,11 @@ func (r *RootCmd) templateCreate() *clibase.Cmd {
}
createReq := codersdk.CreateTemplateRequest{
- Name: templateName,
- VersionID: job.ID,
- DefaultTTLMillis: ptr.Ref(defaultTTL.Milliseconds()),
+ Name: templateName,
+ VersionID: job.ID,
+ DefaultTTLMillis: ptr.Ref(defaultTTL.Milliseconds()),
+ FailureTTLMillis: ptr.Ref(failureTTL.Milliseconds()),
+ InactivityTTLMillis: ptr.Ref(inactivityTTL.Milliseconds()),
}
_, err = client.CreateTemplate(inv.Context(), organization.ID, createReq)
@@ -143,6 +172,18 @@ func (r *RootCmd) templateCreate() *clibase.Cmd {
Default: "24h",
Value: clibase.DurationOf(&defaultTTL),
},
+ {
+ Flag: "failure-ttl",
+ Description: "Specify a failure TTL for workspaces created from this template. This licensed feature's default is 0h (off).",
+ Default: "0h",
+ Value: clibase.DurationOf(&failureTTL),
+ },
+ {
+ Flag: "inactivity-ttl",
+ Description: "Specify an inactivity TTL for workspaces created from this template. This licensed feature's default is 0h (off).",
+ Default: "0h",
+ Value: clibase.DurationOf(&inactivityTTL),
+ },
uploadFlags.option(),
{
Flag: "test.provisioner",
diff --git a/cli/templateedit.go b/cli/templateedit.go
index c4c6e3fd27615..97bc3e13271a3 100644
--- a/cli/templateedit.go
+++ b/cli/templateedit.go
@@ -20,6 +20,8 @@ func (r *RootCmd) templateEdit() *clibase.Cmd {
icon string
defaultTTL time.Duration
maxTTL time.Duration
+ failureTTL time.Duration
+ inactivityTTL time.Duration
allowUserCancelWorkspaceJobs bool
allowUserAutostart bool
allowUserAutostop bool
@@ -34,17 +36,29 @@ func (r *RootCmd) templateEdit() *clibase.Cmd {
),
Short: "Edit the metadata of a template by name.",
Handler: func(inv *clibase.Invocation) error {
- if maxTTL != 0 || !allowUserAutostart || !allowUserAutostop {
+ // This clause can be removed when workspace_actions is no longer experimental
+ if failureTTL != 0 || inactivityTTL != 0 {
+ experiments, exErr := client.Experiments(inv.Context())
+ if exErr != nil {
+ return xerrors.Errorf("get experiments: %w", exErr)
+ }
+
+ if !experiments.Enabled(codersdk.ExperimentWorkspaceActions) {
+ return xerrors.Errorf("--failure-ttl and --inactivityTTL are experimental features. Use the workspace_actions CODER_EXPERIMENTS flag to set these configuration values.")
+ }
+ }
+
+ if maxTTL != 0 || !allowUserAutostart || !allowUserAutostop || failureTTL != 0 || inactivityTTL != 0 {
entitlements, err := client.Entitlements(inv.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, --allow-user-autostart=false or --allow-user-autostop=false")
+ return xerrors.Errorf("your deployment appears to be an AGPL deployment, so you cannot set --max-ttl, --failure-ttl, --inactivityTTL, --allow-user-autostart=false or --allow-user-autostop=false")
} 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, --allow-user-autostart=false or --allow-user-autostop=false")
+ return xerrors.Errorf("your license is not entitled to use advanced template scheduling, so you cannot set --max-ttl, --failure-ttl, --inactivityTTL, --allow-user-autostart=false or --allow-user-autostop=false")
}
}
@@ -65,6 +79,8 @@ func (r *RootCmd) templateEdit() *clibase.Cmd {
Icon: icon,
DefaultTTLMillis: defaultTTL.Milliseconds(),
MaxTTLMillis: maxTTL.Milliseconds(),
+ FailureTTLMillis: failureTTL.Milliseconds(),
+ InactivityTTLMillis: inactivityTTL.Milliseconds(),
AllowUserCancelWorkspaceJobs: allowUserCancelWorkspaceJobs,
AllowUserAutostart: allowUserAutostart,
AllowUserAutostop: allowUserAutostop,
@@ -110,6 +126,18 @@ func (r *RootCmd) templateEdit() *clibase.Cmd {
Description: "Edit the template maximum time before shutdown - workspaces created from this template must shutdown within the given duration after starting. This is an enterprise-only feature.",
Value: clibase.DurationOf(&maxTTL),
},
+ {
+ Flag: "failure-ttl",
+ Description: "Specify a failure TTL for workspaces created from this template. This licensed feature's default is 0h (off).",
+ Default: "0h",
+ Value: clibase.DurationOf(&failureTTL),
+ },
+ {
+ Flag: "inactivity-ttl",
+ Description: "Specify an inactivity TTL for workspaces created from this template. This licensed feature's default is 0h (off).",
+ Default: "0h",
+ Value: clibase.DurationOf(&inactivityTTL),
+ },
{
Flag: "allow-user-cancel-workspace-jobs",
Description: "Allow users to cancel in-progress workspace jobs.",
diff --git a/cli/templateedit_test.go b/cli/templateedit_test.go
index eb84b0ba0f82a..384fc30e7e1ae 100644
--- a/cli/templateedit_test.go
+++ b/cli/templateedit_test.go
@@ -453,6 +453,8 @@ func TestTemplateEdit(t *testing.T) {
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
ctr.DefaultTTLMillis = nil
ctr.MaxTTLMillis = nil
+ ctr.FailureTTLMillis = nil
+ ctr.InactivityTTLMillis = nil
})
// Test the cli command with --allow-user-autostart.
@@ -496,6 +498,8 @@ func TestTemplateEdit(t *testing.T) {
assert.Equal(t, template.MaxTTLMillis, updated.MaxTTLMillis)
assert.Equal(t, template.AllowUserAutostart, updated.AllowUserAutostart)
assert.Equal(t, template.AllowUserAutostop, updated.AllowUserAutostop)
+ assert.Equal(t, template.FailureTTLMillis, updated.FailureTTLMillis)
+ assert.Equal(t, template.InactivityTTLMillis, updated.InactivityTTLMillis)
})
t.Run("BlockedNotEntitled", func(t *testing.T) {
@@ -582,6 +586,8 @@ func TestTemplateEdit(t *testing.T) {
assert.Equal(t, template.MaxTTLMillis, updated.MaxTTLMillis)
assert.Equal(t, template.AllowUserAutostart, updated.AllowUserAutostart)
assert.Equal(t, template.AllowUserAutostop, updated.AllowUserAutostop)
+ assert.Equal(t, template.FailureTTLMillis, updated.FailureTTLMillis)
+ assert.Equal(t, template.InactivityTTLMillis, updated.InactivityTTLMillis)
})
t.Run("Entitled", func(t *testing.T) {
t.Parallel()
@@ -672,6 +678,8 @@ func TestTemplateEdit(t *testing.T) {
assert.Equal(t, template.MaxTTLMillis, updated.MaxTTLMillis)
assert.Equal(t, template.AllowUserAutostart, updated.AllowUserAutostart)
assert.Equal(t, template.AllowUserAutostop, updated.AllowUserAutostop)
+ assert.Equal(t, template.FailureTTLMillis, updated.FailureTTLMillis)
+ assert.Equal(t, template.InactivityTTLMillis, updated.InactivityTTLMillis)
})
})
}
diff --git a/cli/testdata/coder_templates_create_--help.golden b/cli/testdata/coder_templates_create_--help.golden
index 9f4a82816ff58..473f95b0f21f5 100644
--- a/cli/testdata/coder_templates_create_--help.golden
+++ b/cli/testdata/coder_templates_create_--help.golden
@@ -9,6 +9,14 @@ Create a template from the current directory or as specified by flag
-d, --directory string (default: .)
Specify the directory to create from, use '-' to read tar from stdin.
+ --failure-ttl duration (default: 0h)
+ Specify a failure TTL for workspaces created from this template. This
+ licensed feature's default is 0h (off).
+
+ --inactivity-ttl duration (default: 0h)
+ Specify an inactivity TTL for workspaces created from this template.
+ This licensed feature's default is 0h (off).
+
--parameter-file string
Specify a file path with parameter values.
diff --git a/cli/testdata/coder_templates_edit_--help.golden b/cli/testdata/coder_templates_edit_--help.golden
index 271f0d9b9ed56..09c0b7209e78a 100644
--- a/cli/testdata/coder_templates_edit_--help.golden
+++ b/cli/testdata/coder_templates_edit_--help.golden
@@ -24,9 +24,17 @@ Edit the metadata of a template by name.
--display-name string
Edit the template display name.
+ --failure-ttl duration (default: 0h)
+ Specify a failure TTL for workspaces created from this template. This
+ licensed feature's default is 0h (off).
+
--icon string
Edit the template icon path.
+ --inactivity-ttl duration (default: 0h)
+ Specify an inactivity TTL for workspaces created from this template.
+ This licensed feature's default is 0h (off).
+
--max-ttl duration
Edit the template maximum time before shutdown - workspaces created
from this template must shutdown within the given duration after
diff --git a/coderd/templates.go b/coderd/templates.go
index c66c5875217bf..12e62c9a4d059 100644
--- a/coderd/templates.go
+++ b/coderd/templates.go
@@ -214,8 +214,10 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
}
var (
- defaultTTL time.Duration
- maxTTL time.Duration
+ defaultTTL time.Duration
+ maxTTL time.Duration
+ failureTTL time.Duration
+ inactivityTTL time.Duration
)
if createTemplate.DefaultTTLMillis != nil {
defaultTTL = time.Duration(*createTemplate.DefaultTTLMillis) * time.Millisecond
@@ -223,6 +225,12 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
if createTemplate.MaxTTLMillis != nil {
maxTTL = time.Duration(*createTemplate.MaxTTLMillis) * time.Millisecond
}
+ if createTemplate.FailureTTLMillis != nil {
+ failureTTL = time.Duration(*createTemplate.FailureTTLMillis) * time.Millisecond
+ }
+ if createTemplate.InactivityTTLMillis != nil {
+ inactivityTTL = time.Duration(*createTemplate.InactivityTTLMillis) * time.Millisecond
+ }
var validErrs []codersdk.ValidationError
if defaultTTL < 0 {
@@ -234,6 +242,12 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
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 failureTTL < 0 {
+ validErrs = append(validErrs, codersdk.ValidationError{Field: "failure_ttl_ms", Detail: "Must be a positive integer."})
+ }
+ if inactivityTTL < 0 {
+ validErrs = append(validErrs, codersdk.ValidationError{Field: "inactivity_ttl_ms", Detail: "Must be a positive integer."})
+ }
if len(validErrs) > 0 {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Invalid create template request.",
@@ -279,7 +293,12 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
UserAutostartEnabled: allowUserAutostart,
UserAutostopEnabled: allowUserAutostop,
DefaultTTL: defaultTTL,
- MaxTTL: maxTTL,
+ // Some of these values are enterprise-only, but the
+ // TemplateScheduleStore will handle avoiding setting them if
+ // unlicensed.
+ MaxTTL: maxTTL,
+ FailureTTL: failureTTL,
+ InactivityTTL: inactivityTTL,
})
if err != nil {
return xerrors.Errorf("set template schedule options: %s", err)
diff --git a/docs/cli/templates_create.md b/docs/cli/templates_create.md
index 772dfe6335dae..f64857e8c95a1 100644
--- a/docs/cli/templates_create.md
+++ b/docs/cli/templates_create.md
@@ -30,6 +30,24 @@ Specify a default TTL for workspaces created from this template.
Specify the directory to create from, use '-' to read tar from stdin.
+### --failure-ttl
+
+| | |
+| ------- | --------------------- |
+| Type | duration
|
+| Default | 0h
|
+
+Specify a failure TTL for workspaces created from this template. This licensed feature's default is 0h (off).
+
+### --inactivity-ttl
+
+| | |
+| ------- | --------------------- |
+| Type | duration
|
+| Default | 0h
|
+
+Specify an inactivity TTL for workspaces created from this template. This licensed feature's default is 0h (off).
+
### --parameter-file
| | |
diff --git a/docs/cli/templates_edit.md b/docs/cli/templates_edit.md
index 4752df59a0a71..2d25da15b7cc1 100644
--- a/docs/cli/templates_edit.md
+++ b/docs/cli/templates_edit.md
@@ -63,6 +63,15 @@ Edit the template description.
Edit the template display name.
+### --failure-ttl
+
+| | |
+| ------- | --------------------- |
+| Type | duration
|
+| Default | 0h
|
+
+Specify a failure TTL for workspaces created from this template. This licensed feature's default is 0h (off).
+
### --icon
| | |
@@ -71,6 +80,15 @@ Edit the template display name.
Edit the template icon path.
+### --inactivity-ttl
+
+| | |
+| ------- | --------------------- |
+| Type | duration
|
+| Default | 0h
|
+
+Specify an inactivity TTL for workspaces created from this template. This licensed feature's default is 0h (off).
+
### --max-ttl
| | |
diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm.tsx
index 5e15afad4a941..53936751008c2 100644
--- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm.tsx
+++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm.tsx
@@ -65,7 +65,6 @@ export const getValidationSchema = (): Yup.AnyObjectSchema =>
i18next.t("maxTTLMaxError", { ns: "templateSettingsPage" }),
),
failure_ttl_ms: Yup.number()
- .integer()
.min(0, "Failure cleanup days must not be less than 0.")
.test(
"positive-if-enabled",
@@ -80,7 +79,6 @@ export const getValidationSchema = (): Yup.AnyObjectSchema =>
},
),
inactivity_ttl_ms: Yup.number()
- .integer()
.min(0, "Inactivity cleanup days must not be less than 0.")
.test(
"positive-if-enabled",
@@ -349,7 +347,7 @@ export const TemplateScheduleForm: FC = ({
)}
disabled={isSubmitting || !form.values.failure_cleanup_enabled}
fullWidth
- inputProps={{ min: 0, step: 1 }}
+ inputProps={{ min: 0, step: "any" }}
label="Time until cleanup (days)"
variant="outlined"
type="number"
@@ -385,7 +383,7 @@ export const TemplateScheduleForm: FC = ({
isSubmitting || !form.values.inactivity_cleanup_enabled
}
fullWidth
- inputProps={{ min: 0, step: 1 }}
+ inputProps={{ min: 0, step: "any" }}
label="Time until cleanup (days)"
variant="outlined"
type="number"