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
Next Next commit
RED: add failing unit tests
  • Loading branch information
johnstcn committed Jun 3, 2022
commit 17134311a033d075a9b844536d3f8b482e41e8ea
29 changes: 29 additions & 0 deletions cli/autostart_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"os"
"testing"
"time"

"github.com/stretchr/testify/require"

Expand Down Expand Up @@ -158,4 +159,32 @@ 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) {
Copy link
Contributor

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 ?

Copy link
Member Author

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.

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)
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
ctr.MinAutostartIntervalMillis = ptr.Ref(time.Hour.Milliseconds())
})
workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID)
cmdArgs = []string{"autostart", "enable", workspace.Name, "--minute", "*", "--hour", "*"}
)

cmd, root := clitest.New(t, cmdArgs...)
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")
})
}
58 changes: 58 additions & 0 deletions cli/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -62,6 +63,63 @@ 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", "24h",
}
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")
})

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

t.Run("CreateErrInvalidTz", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
Expand Down
33 changes: 33 additions & 0 deletions cli/ttl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,4 +168,37 @@ func TestTTL(t *testing.T) {
err := cmd.Execute()
require.ErrorContains(t, err, "status code 403: forbidden", "unexpected error")
})

t.Run("TemplateMaxTTL", func(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)
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
ctr.MaxTTLMillis = ptr.Ref((8 * time.Hour).Milliseconds())
})
workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
cwr.TTLMillis = ptr.Ref((8 * time.Hour).Milliseconds())
})
cmdArgs = []string{"ttl", "set", workspace.Name, "24h"}
stdoutBuf = &bytes.Buffer{}
)

cmd, root := clitest.New(t, cmdArgs...)
clitest.SetupConfig(t, client, root)
cmd.SetOut(stdoutBuf)

err := cmd.Execute()
require.EqualError(t, err, "TODO what is the error")

// 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)
})
}
12 changes: 8 additions & 4 deletions coderd/coderdtest/coderdtest.go
Original file line number Diff line number Diff line change
Expand Up @@ -322,12 +322,16 @@ func CreateWorkspaceBuild(

// CreateTemplate creates a template with the "echo" provisioner for
// compatibility with testing. The name assigned is randomly generated.
func CreateTemplate(t *testing.T, client *codersdk.Client, organization uuid.UUID, version uuid.UUID) codersdk.Template {
template, err := client.CreateTemplate(context.Background(), organization, codersdk.CreateTemplateRequest{
func CreateTemplate(t *testing.T, client *codersdk.Client, organization uuid.UUID, version uuid.UUID, mutators ...func(*codersdk.CreateTemplateRequest)) codersdk.Template {
req := codersdk.CreateTemplateRequest{
Name: randomUsername(),
Description: randomUsername(),
VersionID: version,
})
}
for _, mut := range mutators {
mut(&req)
}
template, err := client.CreateTemplate(context.Background(), organization, req)
require.NoError(t, err)
return template
}
Expand Down Expand Up @@ -400,7 +404,7 @@ func CreateWorkspace(t *testing.T, client *codersdk.Client, organization uuid.UU
req := codersdk.CreateWorkspaceRequest{
TemplateID: templateID,
Name: randomUsername(),
AutostartSchedule: ptr.Ref("CRON_TZ=US/Central * * * * *"),
AutostartSchedule: ptr.Ref("CRON_TZ=US/Central 30 9 * * 1-5"),
TTLMillis: ptr.Ref((8 * time.Hour).Milliseconds()),
}
for _, mutator := range mutators {
Expand Down
20 changes: 16 additions & 4 deletions coderd/workspaces_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -588,9 +588,10 @@ func TestWorkspaceUpdateTTL(t *testing.T) {
t.Parallel()

testCases := []struct {
name string
ttlMillis *int64
expectedError string
name string
ttlMillis *int64
expectedError string
modifyTemplate func(*codersdk.CreateTemplateRequest)
}{
{
name: "disable ttl",
Expand All @@ -617,19 +618,30 @@ func TestWorkspaceUpdateTTL(t *testing.T) {
ttlMillis: ptr.Ref((24*7*time.Hour + time.Minute).Milliseconds()),
expectedError: "ttl must be less than 7 days",
},
{
name: "above template maximum ttl",
ttlMillis: ptr.Ref((12*time.Hour + time.Minute).Milliseconds()),
expectedError: "TODO what is the error",
modifyTemplate: func(ctr *codersdk.CreateTemplateRequest) { ctr.MaxTTLMillis = ptr.Ref((8 * time.Hour).Milliseconds()) },
},
}

for _, testCase := range testCases {
testCase := testCase
t.Run(testCase.name, func(t *testing.T) {
t.Parallel()

mutators := make([]func(*codersdk.CreateTemplateRequest), 0)
if testCase.modifyTemplate != nil {
mutators = append(mutators, testCase.modifyTemplate)
}
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)
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, mutators...)
workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
cwr.AutostartSchedule = nil
cwr.TTLMillis = nil
Expand Down
9 changes: 9 additions & 0 deletions codersdk/organizations.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,15 @@ type CreateTemplateRequest struct {
// templates, but it doesn't make sense for users.
VersionID uuid.UUID `json:"template_version_id" validate:"required"`
ParameterValues []CreateParameterRequest `json:"parameter_values,omitempty"`

// 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"`
Comment on lines +65 to +72
Copy link
Member Author

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.

Copy link
Contributor

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?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup!

Copy link
Member

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.

Copy link
Member Author

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.

}

// CreateWorkspaceRequest provides options for creating a new workspace.
Expand Down