-
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
feat: enforce template-level constraints for TTL and autostart #2018
Conversation
d4b43ac
to
709db5b
Compare
MaxTTLMillis int64 `json:"max_ttl_ms"` | ||
MinAutostartIntervalMillis int64 `json:"min_autostart_interval_ms"` |
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.
Review: following convention of codersdk
types using int64
millisecond-timestamps for time.Duration
// MaxTTLMillis allows optionally specifying the maximum allowable 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"` |
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.
Making these optional in creation; if preferred I can make them mandatory.
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.
Is this so you don't have some autostart config that says start my workspace every minute?
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.
Yup!
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.
Thought: Can we make he zero values useful here? Or can these be set to zero to disable them entirely? If zero doesn't need to disable, we can treat it as not set, avoiding the use of pointers.
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.
FE prefers to consume null instead of zero-values; hence the pointers.
It would be nice though; I'll open a follow-up PR to investigate this.
var t0 = time.Date(1970, 1, 1, 1, 1, 1, 0, time.UTC) | ||
var tMax = t0.Add(168 * time.Hour) | ||
|
||
// Min returns the minimum duration of the schedule. | ||
// This is calculated as follows: | ||
// - Let t(0) be a given point in time (1970-01-01T01:01:01Z00:00) | ||
// - Let t(max) be 168 hours after t(0). | ||
// - Let t(1) be the next scheduled time after t(0). | ||
// - Let t(n) be the next scheduled time after t(n-1). | ||
// - Then, the minimum duration of s d(min) | ||
// = min( t(n) - t(n-1) ∀ n ∈ N, t(n) < t(max) ) | ||
func (s Schedule) Min() time.Duration { | ||
durMin := tMax.Sub(t0) | ||
tPrev := s.Next(t0) | ||
tCurr := s.Next(tPrev) | ||
for { | ||
dur := tCurr.Sub(tPrev) | ||
if dur < durMin { | ||
durMin = dur | ||
} | ||
tmp := tCurr | ||
tCurr = s.Next(tmp) | ||
tPrev = tmp | ||
if tCurr.After(tMax) { | ||
break | ||
} | ||
} | ||
return durMin | ||
} | ||
|
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.
Review: This is the "simplest" way I could think of to determine the minimum schedule interval without introspecting the actual cron object itself and doing some bit munging.
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.
This makes sense. What is the significance of 168 hrs (1wk)?
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.
@Emyrk it's the maximum ttl (system-constraint)
@@ -2,7 +2,6 @@ package executor_test | |||
|
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.
Review: I ended up making a lot of changes in this test because I had been using a simple * * * * *
schedule which is lower than the now-enforced default 🙃
cli/create_test.go
Outdated
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, "schedule: parse schedule: provided bad location invalid: unknown time zone invalid") |
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.
Review: the parallelism isn't strictly necessary in this test as I don't need to do I/O with the pty, so just leaving it out for simplicity.
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") |
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.
Review: as above, removing parallelism from this test as it's not strictly necessary.
"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 comment
The 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
@@ -262,7 +265,22 @@ 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 comment
The 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.
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) | ||
} | ||
|
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.
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 comment
The 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 comment
The 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 create
command the user ends up seeing the workspace dry run, confirming the create, and then getting the validation error, which is annoying.
And yes, this is definitely a good candidate to be DRYed up a bit.
Thinking about this more, would an engineer really want an Auto-Off that is less than the template-configured amount? Since the engineer is not accountable to infrastructure costs like the template admin, I think not. Perhaps you've made the |
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.
Other than the one correctness question, I think the code changes look good. 👍🏻
@@ -108,6 +108,36 @@ func (s Schedule) Next(t time.Time) time.Time { | |||
return s.sched.Next(t) | |||
} | |||
|
|||
var t0 = time.Date(1970, 1, 1, 1, 1, 1, 0, time.UTC) | |||
var tMax = t0.Add(168 * time.Hour) |
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.
Would this work correctly (or as expected) if e.g. max TTL is set to a different value in the database (i.e. the column that's added to templates
)?
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.
We don't let people set the month
or day-of-month
field currently, so a schedule currently maxes out at 1 week.
// MaxTTLMillis allows optionally specifying the maximum allowable 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"` |
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.
Thought: Can we make he zero values useful here? Or can these be set to zero to disable them entirely? If zero doesn't need to disable, we can treat it as not set, avoiding the use of pointers.
@@ -158,4 +159,26 @@ func TestAutostart(t *testing.T) { | |||
require.NoError(t, err, "fetch updated workspace") | |||
require.Equal(t, expectedSchedule, *updated.AutostartSchedule, "expected default autostart schedule") | |||
}) | |||
|
|||
t.Run("BelowTemplateConstraint", func(t *testing.T) { |
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.
@@ -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 comment
The reason will be displayed to describe this comment to others. Learn more.
👍🏻
"--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 comment
The reason will be displayed to describe this comment to others. Learn more.
👍🏻
var t0 = time.Date(1970, 1, 1, 1, 1, 1, 0, time.UTC) | ||
var tMax = t0.Add(168 * time.Hour) | ||
|
||
// Min returns the minimum duration of the schedule. | ||
// This is calculated as follows: | ||
// - Let t(0) be a given point in time (1970-01-01T01:01:01Z00:00) | ||
// - Let t(max) be 168 hours after t(0). | ||
// - Let t(1) be the next scheduled time after t(0). | ||
// - Let t(n) be the next scheduled time after t(n-1). | ||
// - Then, the minimum duration of s d(min) | ||
// = min( t(n) - t(n-1) ∀ n ∈ N, t(n) < t(max) ) | ||
func (s Schedule) Min() time.Duration { | ||
durMin := tMax.Sub(t0) | ||
tPrev := s.Next(t0) | ||
tCurr := s.Next(tPrev) | ||
for { | ||
dur := tCurr.Sub(tPrev) | ||
if dur < durMin { | ||
durMin = dur | ||
} | ||
tmp := tCurr | ||
tCurr = s.Next(tmp) | ||
tPrev = tmp | ||
if tCurr.After(tMax) { | ||
break | ||
} | ||
} | ||
return durMin | ||
} | ||
|
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.
@Emyrk it's the maximum ttl (system-constraint)
@@ -243,6 +245,8 @@ export interface Template { | |||
readonly active_version_id: string | |||
readonly workspace_owner_count: number | |||
readonly description: string | |||
readonly max_ttl_ms: number | |||
readonly min_autostart_interval_ms: number |
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(for FE): Could I use these to set min/max and validations on the WorkspaceScheduleForm
?
I think I can pass the template down into those components and add that logic. Is there any concerns or gotchas or notes about me doing that @johnstcn ?
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 think that's a good idea!
…int-for-workspace-scheduling
…constraint-for-workspace-scheduling
This PR adds fields to templates that constrain values for workspaces derived from that template. - Autostop: Adds a field max_ttl on the template which limits the maximum value of ttl on all workspaces derived from that template. Defaulting to 168 hours, enforced on edits to workspace metadata. New workspaces will default to the templates's `max_ttl` if not specified. - Autostart: Adds a field min_autostart_duration which limits the minimum duration between successive autostarts of a template, measured from a single reference time. Defaulting to 1 hour, enforced on edits to workspace metadata.
This PR adds fields to templates that constrain values for workspaces derived from that template.