-
Notifications
You must be signed in to change notification settings - Fork 886
feat: enforce template-level constraints for TTL and autostart #2018
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1713431
f11f942
c5c9a7a
74f4dee
7da27a0
5ad048b
65b980e
aa298a6
a5ec9bf
a6255d6
245795e
709db5b
c4a0927
244e479
60ef479
c9aab8b
f997af2
d7f697b
ebe4c15
08ef2eb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -61,20 +61,6 @@ func create() *cobra.Command { | |
} | ||
} | ||
|
||
tz, err := time.LoadLocation(tzName) | ||
if err != nil { | ||
return xerrors.Errorf("Invalid workspace autostart timezone: %w", err) | ||
} | ||
johnstcn marked this conversation as resolved.
Show resolved
Hide resolved
|
||
schedSpec := fmt.Sprintf("CRON_TZ=%s %s %s * * %s", tz.String(), autostartMinute, autostartHour, autostartDow) | ||
_, err = schedule.Weekly(schedSpec) | ||
if err != nil { | ||
return xerrors.Errorf("invalid workspace autostart schedule: %w", err) | ||
} | ||
|
||
if ttl == 0 { | ||
return xerrors.Errorf("TTL must be at least 1 minute") | ||
} | ||
|
||
_, err = client.WorkspaceByOwnerAndName(cmd.Context(), codersdk.Me, workspaceName) | ||
if err == nil { | ||
return xerrors.Errorf("A workspace already exists named %q!", workspaceName) | ||
|
@@ -129,6 +115,23 @@ func create() *cobra.Command { | |
} | ||
} | ||
|
||
schedSpec, err := validSchedule( | ||
autostartMinute, | ||
autostartHour, | ||
autostartDow, | ||
tzName, | ||
time.Duration(template.MinAutostartIntervalMillis)*time.Millisecond, | ||
) | ||
if err != nil { | ||
return xerrors.Errorf("Invalid autostart schedule: %w", err) | ||
} | ||
if ttl < time.Minute { | ||
return xerrors.Errorf("TTL must be at least 1 minute") | ||
} | ||
if ttlMax := time.Duration(template.MaxTTLMillis) * time.Millisecond; ttl > ttlMax { | ||
return xerrors.Errorf("TTL must be below template maximum %s", ttlMax) | ||
} | ||
|
||
Comment on lines
+118
to
+134
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Review: We're duplicating some validations here because I want the errors to show up before the whole workspace plan happens. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Interesting to do it in the cli. Can we somehow put the validation on a datastructure and reuse it for the backend + frontend? Like reuse the same code? Not a huge deal. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It gets validated on the BE as well but if we don't at least validate in the And yes, this is definitely a good candidate to be DRYed up a bit. |
||
templateVersion, err := client.TemplateVersion(cmd.Context(), template.ActiveVersionID) | ||
if err != nil { | ||
return err | ||
|
@@ -226,7 +229,7 @@ func create() *cobra.Command { | |
workspace, err := client.CreateWorkspace(cmd.Context(), organization.ID, codersdk.CreateWorkspaceRequest{ | ||
TemplateID: template.ID, | ||
Name: workspaceName, | ||
AutostartSchedule: &schedSpec, | ||
AutostartSchedule: schedSpec, | ||
TTLMillis: ptr.Ref(ttl.Milliseconds()), | ||
ParameterValues: parameters, | ||
}) | ||
|
@@ -262,7 +265,27 @@ func create() *cobra.Command { | |
cliflag.StringVarP(cmd.Flags(), &autostartMinute, "autostart-minute", "", "CODER_WORKSPACE_AUTOSTART_MINUTE", "0", "Specify the minute(s) at which the workspace should autostart (e.g. 0).") | ||
cliflag.StringVarP(cmd.Flags(), &autostartHour, "autostart-hour", "", "CODER_WORKSPACE_AUTOSTART_HOUR", "9", "Specify the hour(s) at which the workspace should autostart (e.g. 9).") | ||
cliflag.StringVarP(cmd.Flags(), &autostartDow, "autostart-day-of-week", "", "CODER_WORKSPACE_AUTOSTART_DOW", "MON-FRI", "Specify the days(s) on which the workspace should autostart (e.g. MON,TUE,WED,THU,FRI)") | ||
cliflag.StringVarP(cmd.Flags(), &tzName, "tz", "", "TZ", "", "Specify your timezone location for workspace autostart (e.g. US/Central).") | ||
cliflag.StringVarP(cmd.Flags(), &tzName, "tz", "", "TZ", "UTC", "Specify your timezone location for workspace autostart (e.g. US/Central).") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Review: defaulting TZ to UTC here explicitly. Will hopefully have some fancier auto-detection in a separate issue. |
||
cliflag.DurationVarP(cmd.Flags(), &ttl, "ttl", "", "CODER_WORKSPACE_TTL", 8*time.Hour, "Specify a time-to-live (TTL) for the workspace (e.g. 8h).") | ||
return cmd | ||
} | ||
|
||
func validSchedule(minute, hour, dow, tzName string, min time.Duration) (*string, error) { | ||
_, err := time.LoadLocation(tzName) | ||
if err != nil { | ||
return nil, xerrors.Errorf("Invalid workspace autostart timezone: %w", err) | ||
} | ||
|
||
schedSpec := fmt.Sprintf("CRON_TZ=%s %s %s * * %s", tzName, minute, hour, dow) | ||
|
||
sched, err := schedule.Weekly(schedSpec) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if schedMin := sched.Min(); schedMin < min { | ||
return nil, xerrors.Errorf("minimum autostart interval %s is above template constraint %s", schedMin, min) | ||
} | ||
|
||
return &schedSpec, nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,6 +14,7 @@ import ( | |
"github.com/coder/coder/cli/clitest" | ||
"github.com/coder/coder/coderd/coderdtest" | ||
"github.com/coder/coder/coderd/database" | ||
"github.com/coder/coder/coderd/util/ptr" | ||
"github.com/coder/coder/codersdk" | ||
"github.com/coder/coder/provisioner/echo" | ||
"github.com/coder/coder/provisionersdk/proto" | ||
|
@@ -62,6 +63,57 @@ func TestCreate(t *testing.T) { | |
<-doneChan | ||
}) | ||
|
||
t.Run("AboveTemplateMaxTTL", func(t *testing.T) { | ||
t.Parallel() | ||
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) | ||
user := coderdtest.CreateFirstUser(t, client) | ||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) | ||
coderdtest.AwaitTemplateVersionJob(t, client, version.ID) | ||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { | ||
ctr.MaxTTLMillis = ptr.Ref((12 * time.Hour).Milliseconds()) | ||
}) | ||
args := []string{ | ||
"create", | ||
"my-workspace", | ||
"--template", template.Name, | ||
"--ttl", "12h1m", | ||
"-y", // don't bother with waiting | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Review: I'm slightly side-stepping here because I don't want to deal with confirms |
||
} | ||
cmd, root := clitest.New(t, args...) | ||
clitest.SetupConfig(t, client, root) | ||
pty := ptytest.New(t) | ||
cmd.SetIn(pty.Input()) | ||
cmd.SetOut(pty.Output()) | ||
err := cmd.Execute() | ||
assert.ErrorContains(t, err, "TTL must be below template maximum 12h0m0s") | ||
}) | ||
|
||
t.Run("BelowTemplateMinAutostartInterval", func(t *testing.T) { | ||
t.Parallel() | ||
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) | ||
user := coderdtest.CreateFirstUser(t, client) | ||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) | ||
coderdtest.AwaitTemplateVersionJob(t, client, version.ID) | ||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { | ||
ctr.MinAutostartIntervalMillis = ptr.Ref(time.Hour.Milliseconds()) | ||
}) | ||
args := []string{ | ||
"create", | ||
"my-workspace", | ||
"--template", template.Name, | ||
"--autostart-minute", "*", // Every minute | ||
"--autostart-hour", "*", // Every hour | ||
"-y", // don't bother with waiting | ||
} | ||
cmd, root := clitest.New(t, args...) | ||
clitest.SetupConfig(t, client, root) | ||
pty := ptytest.New(t) | ||
cmd.SetIn(pty.Input()) | ||
cmd.SetOut(pty.Output()) | ||
err := cmd.Execute() | ||
assert.ErrorContains(t, err, "minimum autostart interval 1m0s is above template constraint 1h0m0s") | ||
}) | ||
|
||
t.Run("CreateErrInvalidTz", func(t *testing.T) { | ||
t.Parallel() | ||
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) | ||
|
@@ -74,19 +126,15 @@ func TestCreate(t *testing.T) { | |
"my-workspace", | ||
"--template", template.Name, | ||
"--tz", "invalid", | ||
"-y", | ||
} | ||
cmd, root := clitest.New(t, args...) | ||
clitest.SetupConfig(t, client, root) | ||
doneChan := make(chan struct{}) | ||
pty := ptytest.New(t) | ||
cmd.SetIn(pty.Input()) | ||
cmd.SetOut(pty.Output()) | ||
go func() { | ||
defer close(doneChan) | ||
err := cmd.Execute() | ||
assert.EqualError(t, err, "Invalid workspace autostart timezone: unknown time zone invalid") | ||
}() | ||
<-doneChan | ||
err := cmd.Execute() | ||
assert.ErrorContains(t, err, "Invalid autostart schedule: Invalid workspace autostart timezone: unknown time zone invalid") | ||
}) | ||
|
||
t.Run("CreateErrInvalidTTL", func(t *testing.T) { | ||
|
@@ -101,19 +149,15 @@ func TestCreate(t *testing.T) { | |
"my-workspace", | ||
"--template", template.Name, | ||
"--ttl", "0s", | ||
"-y", | ||
} | ||
cmd, root := clitest.New(t, args...) | ||
clitest.SetupConfig(t, client, root) | ||
doneChan := make(chan struct{}) | ||
pty := ptytest.New(t) | ||
cmd.SetIn(pty.Input()) | ||
cmd.SetOut(pty.Output()) | ||
go func() { | ||
defer close(doneChan) | ||
err := cmd.Execute() | ||
assert.EqualError(t, err, "TTL must be at least 1 minute") | ||
}() | ||
<-doneChan | ||
err := cmd.Execute() | ||
assert.EqualError(t, err, "TTL must be at least 1 minute") | ||
Comment on lines
-111
to
+160
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Review: as above, removing parallelism from this test as it's not strictly necessary. |
||
}) | ||
|
||
t.Run("CreateFromListWithSkip", func(t *testing.T) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,16 +14,19 @@ import ( | |
|
||
"github.com/coder/coder/cli/cliui" | ||
"github.com/coder/coder/coderd/database" | ||
"github.com/coder/coder/coderd/util/ptr" | ||
"github.com/coder/coder/codersdk" | ||
"github.com/coder/coder/provisionerd" | ||
"github.com/coder/coder/provisionersdk" | ||
) | ||
|
||
func templateCreate() *cobra.Command { | ||
var ( | ||
directory string | ||
provisioner string | ||
parameterFile string | ||
directory string | ||
provisioner string | ||
parameterFile string | ||
maxTTL time.Duration | ||
minAutostartInterval time.Duration | ||
) | ||
cmd := &cobra.Command{ | ||
Use: "create [name]", | ||
|
@@ -92,11 +95,15 @@ func templateCreate() *cobra.Command { | |
return err | ||
} | ||
|
||
_, err = client.CreateTemplate(cmd.Context(), organization.ID, codersdk.CreateTemplateRequest{ | ||
Name: templateName, | ||
VersionID: job.ID, | ||
ParameterValues: parameters, | ||
}) | ||
createReq := codersdk.CreateTemplateRequest{ | ||
Name: templateName, | ||
VersionID: job.ID, | ||
ParameterValues: parameters, | ||
MaxTTLMillis: ptr.Ref(maxTTL.Milliseconds()), | ||
MinAutostartIntervalMillis: ptr.Ref(minAutostartInterval.Milliseconds()), | ||
} | ||
|
||
_, err = client.CreateTemplate(cmd.Context(), organization.ID, createReq) | ||
if err != nil { | ||
return err | ||
} | ||
|
@@ -115,6 +122,8 @@ 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", "", 168*time.Hour, "Specify a maximum TTL for worksapces created from this template.") | ||
cmd.Flags().DurationVarP(&minAutostartInterval, "min-autostart-interval", "", time.Hour, "Specify a minimum autostart interval for workspaces created from this template.") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍🏻 |
||
// This is for testing! | ||
err := cmd.Flags().MarkHidden("test.provisioner") | ||
if err != nil { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,7 +24,16 @@ func TestTemplateCreate(t *testing.T) { | |
Parse: echo.ParseComplete, | ||
Provision: echo.ProvisionComplete, | ||
}) | ||
cmd, root := clitest.New(t, "templates", "create", "my-template", "--directory", source, "--test.provisioner", string(database.ProvisionerTypeEcho)) | ||
args := []string{ | ||
"templates", | ||
"create", | ||
"my-template", | ||
"--directory", source, | ||
"--test.provisioner", string(database.ProvisionerTypeEcho), | ||
"--max-ttl", "24h", | ||
"--min-autostart-interval", "2h", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍🏻 |
||
} | ||
cmd, root := clitest.New(t, args...) | ||
clitest.SetupConfig(t, client, root) | ||
pty := ptytest.New(t) | ||
cmd.SetIn(pty.Input()) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question(gut-check): Should there be a similar case for AboveTemplateConstraint ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see any reason to enforce an upper bound on the interval between successive autostarts.