feat: add workspace actions cleanup configuration flags to CLI template create and edit (#7453)

* added cleanup flags on template create

* added cleanup flags on template edit

* fixed tests

* added to tests
This commit is contained in:
Kira Pilot 2023-05-10 12:57:11 -07:00 committed by GitHub
parent 816c37dd0d
commit a42a36a474
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 159 additions and 13 deletions

View File

@ -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",

View File

@ -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.",

View File

@ -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)
})
})
}

View File

@ -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.

View File

@ -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

View File

@ -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)

View File

@ -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 | <code>duration</code> |
| Default | <code>0h</code> |
Specify a failure TTL for workspaces created from this template. This licensed feature's default is 0h (off).
### --inactivity-ttl
| | |
| ------- | --------------------- |
| Type | <code>duration</code> |
| Default | <code>0h</code> |
Specify an inactivity TTL for workspaces created from this template. This licensed feature's default is 0h (off).
### --parameter-file
| | |

View File

@ -63,6 +63,15 @@ Edit the template description.
Edit the template display name.
### --failure-ttl
| | |
| ------- | --------------------- |
| Type | <code>duration</code> |
| Default | <code>0h</code> |
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 | <code>duration</code> |
| Default | <code>0h</code> |
Specify an inactivity TTL for workspaces created from this template. This licensed feature's default is 0h (off).
### --max-ttl
| | |

View File

@ -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",
@ -344,7 +342,7 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
)}
disabled={isSubmitting || !form.values.failure_cleanup_enabled}
fullWidth
inputProps={{ min: 0, step: 1 }}
inputProps={{ min: 0, step: "any" }}
label="Time until cleanup (days)"
type="number"
aria-label="Failure Cleanup"
@ -378,7 +376,7 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
isSubmitting || !form.values.inactivity_cleanup_enabled
}
fullWidth
inputProps={{ min: 0, step: 1 }}
inputProps={{ min: 0, step: "any" }}
label="Time until cleanup (days)"
type="number"
aria-label="Inactivity Cleanup"