Thanks to visit codestin.com
Credit goes to github.com

Skip to content

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

Merged
merged 20 commits into from
Jun 7, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
GREEN: fix even more tests
  • Loading branch information
johnstcn committed Jun 3, 2022
commit a5ec9bf90e4219fbfdd2f30e9cafb7a1c45e4d00
8 changes: 1 addition & 7 deletions cli/autostart_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,6 @@ func TestAutostart(t *testing.T) {
t.Parallel()

var (
ctx = context.Background()
client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
user = coderdtest.CreateFirstUser(t, client)
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
Expand All @@ -180,11 +179,6 @@ func TestAutostart(t *testing.T) {
clitest.SetupConfig(t, client, root)

err := cmd.Execute()
require.NoError(t, err, "unexpected error")

// Ensure nothing happened
updated, err := client.Workspace(ctx, workspace.ID)
require.NoError(t, err, "fetch updated workspace")
require.Equal(t, *workspace.AutostartSchedule, *updated.AutostartSchedule, "expected previous autostart schedule")
require.ErrorContains(t, err, "schedule: Minimum autostart interval 1m0s below template minimum 1h0m0s")
})
}
33 changes: 16 additions & 17 deletions cli/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (

"github.com/coder/coder/cli/cliflag"
"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/coderd/autobuild/schedule"
"github.com/coder/coder/coderd/util/ptr"
"github.com/coder/coder/codersdk"
)
Expand Down Expand Up @@ -61,20 +60,6 @@ func create() *cobra.Command {
}
}

tz, err := time.LoadLocation(tzName)
if err != nil {
return xerrors.Errorf("Invalid workspace autostart timezone: %w", err)
}
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(), organization.ID, codersdk.Me, workspaceName)
if err == nil {
return xerrors.Errorf("A workspace already exists named %q!", workspaceName)
Expand Down Expand Up @@ -129,6 +114,11 @@ func create() *cobra.Command {
}
}

schedSpec := buildSchedule(autostartMinute, autostartHour, autostartDow, tzName)
if ttl < time.Minute {
return xerrors.Errorf("TTL must be at least 1 minute")
}

templateVersion, err := client.TemplateVersion(cmd.Context(), template.ActiveVersionID)
if err != nil {
return err
Expand Down Expand Up @@ -226,7 +216,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,
})
Expand Down Expand Up @@ -262,7 +252,16 @@ 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).")
Copy link
Member Author

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.

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 buildSchedule(minute, hour, dow, tzName string) *string {
if minute == "" || hour == "" || dow == "" {
return nil
}

schedSpec := fmt.Sprintf("CRON_TZ=%s %s %s * * %s", tzName, minute, hour, dow)
return &schedSpec
}
44 changes: 13 additions & 31 deletions cli/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import (

func TestCreate(t *testing.T) {
t.Parallel()
t.Fatal("CIAN FIX THIS")
t.Run("Create", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
Expand Down Expand Up @@ -66,7 +65,6 @@ func TestCreate(t *testing.T) {

t.Run("AboveTemplateMaxTTL", func(t *testing.T) {
t.Parallel()
t.Fatal("CIAN FIX THIS")
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
Expand All @@ -78,24 +76,20 @@ func TestCreate(t *testing.T) {
"create",
"my-workspace",
"--template", template.Name,
"--ttl", "24h",
"--ttl", "12h1m",
"-y", // don't bother with waiting
Copy link
Member Author

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

}
cmd, root := clitest.New(t, args...)
clitest.SetupConfig(t, client, root)
errCh := make(chan error)
pty := ptytest.New(t)
cmd.SetIn(pty.Input())
cmd.SetOut(pty.Output())
go func() {
defer close(errCh)
errCh <- cmd.Execute()
}()
require.EqualError(t, <-errCh, "TODO what is the error")
err := cmd.Execute()
assert.ErrorContains(t, err, "ttl_ms: ttl must be below template maximum 12h0m0s")
})

t.Run("BelowTemplateMinAutostartInterval", func(t *testing.T) {
t.Parallel()
t.Fatal("CIAN FIX THIS")
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
Expand All @@ -109,18 +103,15 @@ func TestCreate(t *testing.T) {
"--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)
errCh := make(chan error)
pty := ptytest.New(t)
cmd.SetIn(pty.Input())
cmd.SetOut(pty.Output())
go func() {
defer close(errCh)
errCh <- cmd.Execute()
}()
require.EqualError(t, <-errCh, "TODO what is the error")
err := cmd.Execute()
assert.ErrorContains(t, err, "Minimum autostart interval 1m0s below template minimum 1h0m0s")
})

t.Run("CreateErrInvalidTz", func(t *testing.T) {
Expand All @@ -135,24 +126,19 @@ 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, "schedule: parse schedule: provided bad location invalid: unknown time zone invalid")
Copy link
Member Author

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.

})

t.Run("CreateErrInvalidTTL", func(t *testing.T) {
t.Parallel()
t.Fatal("CIAN FIX THIS")
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
Expand All @@ -163,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
Copy link
Member Author

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.

})

t.Run("CreateFromListWithSkip", func(t *testing.T) {
Expand Down
4 changes: 2 additions & 2 deletions cli/ttl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,12 +193,12 @@ func TestTTL(t *testing.T) {
cmd.SetOut(stdoutBuf)

err := cmd.Execute()
require.EqualError(t, err, "TODO what is the error")
require.ErrorContains(t, err, "ttl_ms: ttl must be below template maximum 8h0m0s")

// Ensure ttl not updated
updated, err := client.Workspace(ctx, workspace.ID)
require.NoError(t, err, "fetch updated workspace")
require.NotNil(t, updated.TTLMillis)
require.Equal(t, (8 * time.Hour).Milliseconds, *updated.TTLMillis)
require.Equal(t, (8 * time.Hour).Milliseconds(), *updated.TTLMillis)
})
}
30 changes: 11 additions & 19 deletions coderd/workspaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
return
}

dbTTL, err := validWorkspaceTTLMillis(createWorkspace.TTLMillis)
dbTTL, err := validWorkspaceTTLMillis(createWorkspace.TTLMillis, time.Duration(template.MaxTtl))
if err != nil {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: "Invalid Workspace TTL",
Expand Down Expand Up @@ -560,20 +560,6 @@ func (api *API) putWorkspaceTTL(rw http.ResponseWriter, r *http.Request) {
return
}

dbTTL, err := validWorkspaceTTLMillis(req.TTLMillis)
if err != nil {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: "validate workspace ttl",
Errors: []httpapi.Error{
{
Field: "ttl_ms",
Detail: err.Error(),
},
},
})
return
}

template, err := api.Database.GetTemplateByID(r.Context(), workspace.TemplateID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Expand All @@ -582,16 +568,18 @@ func (api *API) putWorkspaceTTL(rw http.ResponseWriter, r *http.Request) {
return
}

if dbTTL.Valid && dbTTL.Int64 > template.MaxTtl {
dbTTL, err := validWorkspaceTTLMillis(req.TTLMillis, time.Duration(template.MaxTtl))
if err != nil {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: "Constrained by template",
Message: "Invalid workspace TTL",
Errors: []httpapi.Error{
{
Field: "ttl_ms",
Detail: fmt.Sprintf("requested value is %s but template max is %s", time.Duration(dbTTL.Int64), time.Duration(template.MaxTtl)),
Detail: err.Error(),
},
},
})
return
}

err = api.Database.UpdateWorkspaceTTL(r.Context(), database.UpdateWorkspaceTTLParams{
Expand Down Expand Up @@ -883,7 +871,7 @@ func convertWorkspaceTTLMillis(i sql.NullInt64) *int64 {
return &millis
}

func validWorkspaceTTLMillis(millis *int64) (sql.NullInt64, error) {
func validWorkspaceTTLMillis(millis *int64, max time.Duration) (sql.NullInt64, error) {
if ptr.NilOrZero(millis) {
return sql.NullInt64{}, nil
}
Expand All @@ -898,6 +886,10 @@ func validWorkspaceTTLMillis(millis *int64) (sql.NullInt64, error) {
return sql.NullInt64{}, xerrors.New("ttl must be less than 7 days")
}

if truncated > max {
return sql.NullInt64{}, xerrors.Errorf("ttl must be below template maximum %s", max.String())
}

return sql.NullInt64{
Valid: true,
Int64: int64(truncated),
Expand Down
2 changes: 1 addition & 1 deletion coderd/workspaces_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -647,7 +647,7 @@ func TestWorkspaceUpdateTTL(t *testing.T) {
{
name: "above template maximum ttl",
ttlMillis: ptr.Ref((12 * time.Hour).Milliseconds()),
expectedError: "requested value is 12h0m0s but template max is 8h0m0s",
expectedError: "ttl_ms: ttl must be below template maximum 8h0m0s",
modifyTemplate: func(ctr *codersdk.CreateTemplateRequest) { ctr.MaxTTLMillis = ptr.Ref((8 * time.Hour).Milliseconds()) },
},
}
Expand Down