mirror of https://github.com/coder/coder.git
feat: add default autostart and ttl for new workspaces (#1632)
* database: add autostart_schedule and ttl to InsertWorkspace; make gen * coderd: workspaces: consume additional fields of CreateWorkspaceRequest * cli: update: add support for TTL and autostart_schedule * cli: create: add unit tests * coder: import `time/tzdata` for embedded timezone database * autobuild: fix unit test that only runs with a real db
This commit is contained in:
parent
c465f8a8a3
commit
b2020761d9
|
@ -10,14 +10,20 @@ import (
|
|||
|
||||
"github.com/coder/coder/cli/cliflag"
|
||||
"github.com/coder/coder/cli/cliui"
|
||||
"github.com/coder/coder/coderd/autobuild/schedule"
|
||||
"github.com/coder/coder/codersdk"
|
||||
)
|
||||
|
||||
func create() *cobra.Command {
|
||||
var (
|
||||
workspaceName string
|
||||
templateName string
|
||||
parameterFile string
|
||||
autostartMinute string
|
||||
autostartHour string
|
||||
autostartDow string
|
||||
parameterFile string
|
||||
templateName string
|
||||
ttl time.Duration
|
||||
tzName string
|
||||
workspaceName string
|
||||
)
|
||||
cmd := &cobra.Command{
|
||||
Annotations: workspaceCommand,
|
||||
|
@ -54,6 +60,20 @@ 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)
|
||||
|
@ -174,9 +194,11 @@ func create() *cobra.Command {
|
|||
|
||||
before := time.Now()
|
||||
workspace, err := client.CreateWorkspace(cmd.Context(), organization.ID, codersdk.CreateWorkspaceRequest{
|
||||
TemplateID: template.ID,
|
||||
Name: workspaceName,
|
||||
ParameterValues: parameters,
|
||||
TemplateID: template.ID,
|
||||
Name: workspaceName,
|
||||
AutostartSchedule: &schedSpec,
|
||||
TTL: &ttl,
|
||||
ParameterValues: parameters,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -207,5 +229,10 @@ func create() *cobra.Command {
|
|||
cliui.AllowSkipPrompt(cmd)
|
||||
cliflag.StringVarP(cmd.Flags(), &templateName, "template", "t", "CODER_TEMPLATE_NAME", "", "Specify a template name.")
|
||||
cliflag.StringVarP(cmd.Flags(), ¶meterFile, "parameter-file", "", "CODER_PARAMETER_FILE", "", "Specify a file path with parameter values.")
|
||||
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.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
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/cli/clitest"
|
||||
|
@ -25,7 +26,17 @@ func TestCreate(t *testing.T) {
|
|||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
cmd, root := clitest.New(t, "create", "my-workspace", "--template", template.Name)
|
||||
args := []string{
|
||||
"create",
|
||||
"my-workspace",
|
||||
"--template", template.Name,
|
||||
"--tz", "US/Central",
|
||||
"--autostart-minute", "0",
|
||||
"--autostart-hour", "*/2",
|
||||
"--autostart-day-of-week", "MON-FRI",
|
||||
"--ttl", "8h",
|
||||
}
|
||||
cmd, root := clitest.New(t, args...)
|
||||
clitest.SetupConfig(t, client, root)
|
||||
doneChan := make(chan struct{})
|
||||
pty := ptytest.New(t)
|
||||
|
@ -48,6 +59,60 @@ func TestCreate(t *testing.T) {
|
|||
<-doneChan
|
||||
})
|
||||
|
||||
t.Run("CreateErrInvalidTz", 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)
|
||||
args := []string{
|
||||
"create",
|
||||
"my-workspace",
|
||||
"--template", template.Name,
|
||||
"--tz", "invalid",
|
||||
}
|
||||
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
|
||||
})
|
||||
|
||||
t.Run("CreateErrInvalidTTL", 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)
|
||||
args := []string{
|
||||
"create",
|
||||
"my-workspace",
|
||||
"--template", template.Name,
|
||||
"--ttl", "0s",
|
||||
}
|
||||
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
|
||||
})
|
||||
|
||||
t.Run("CreateFromListWithSkip", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
_ "time/tzdata"
|
||||
|
||||
"github.com/coder/coder/cli"
|
||||
"github.com/coder/coder/cli/cliui"
|
||||
|
|
|
@ -36,9 +36,6 @@ func TestExecutorAutostartOK(t *testing.T) {
|
|||
// Given: workspace is stopped
|
||||
workspace = mustTransitionWorkspace(t, client, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
|
||||
|
||||
// Given: the workspace initially has autostart disabled
|
||||
require.Empty(t, workspace.AutostartSchedule)
|
||||
|
||||
// When: we enable workspace autostart
|
||||
sched, err := schedule.Weekly("* * * * *")
|
||||
require.NoError(t, err)
|
||||
|
@ -77,9 +74,6 @@ func TestExecutorAutostartTemplateUpdated(t *testing.T) {
|
|||
// Given: workspace is stopped
|
||||
workspace = mustTransitionWorkspace(t, client, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
|
||||
|
||||
// Given: the workspace initially has autostart disabled
|
||||
require.Empty(t, workspace.AutostartSchedule)
|
||||
|
||||
// Given: the workspace template has been updated
|
||||
orgs, err := client.OrganizationsByUser(ctx, workspace.OwnerID.String())
|
||||
require.NoError(t, err)
|
||||
|
@ -131,9 +125,6 @@ func TestExecutorAutostartAlreadyRunning(t *testing.T) {
|
|||
// Given: we ensure the workspace is running
|
||||
require.Equal(t, codersdk.WorkspaceTransitionStart, workspace.LatestBuild.Transition)
|
||||
|
||||
// Given: the workspace initially has autostart disabled
|
||||
require.Empty(t, workspace.AutostartSchedule)
|
||||
|
||||
// When: we enable workspace autostart
|
||||
sched, err := schedule.Weekly("* * * * *")
|
||||
require.NoError(t, err)
|
||||
|
@ -164,15 +155,17 @@ func TestExecutorAutostartNotEnabled(t *testing.T) {
|
|||
IncludeProvisionerD: true,
|
||||
})
|
||||
// Given: we have a user with a workspace
|
||||
workspace = mustProvisionWorkspace(t, client)
|
||||
workspace = mustProvisionWorkspace(t, client, func(cwr *codersdk.CreateWorkspaceRequest) {
|
||||
cwr.AutostartSchedule = nil
|
||||
})
|
||||
)
|
||||
|
||||
// Given: workspace does not have autostart enabled
|
||||
require.Empty(t, workspace.AutostartSchedule)
|
||||
|
||||
// Given: workspace is stopped
|
||||
workspace = mustTransitionWorkspace(t, client, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
|
||||
|
||||
// Given: the workspace has autostart disabled
|
||||
require.Empty(t, workspace.AutostartSchedule)
|
||||
|
||||
// When: the autobuild executor ticks
|
||||
go func() {
|
||||
tickCh <- time.Now().UTC().Add(time.Minute)
|
||||
|
@ -190,8 +183,6 @@ func TestExecutorAutostopOK(t *testing.T) {
|
|||
t.Parallel()
|
||||
|
||||
var (
|
||||
ctx = context.Background()
|
||||
err error
|
||||
tickCh = make(chan time.Time)
|
||||
client = coderdtest.New(t, &coderdtest.Options{
|
||||
AutobuildTicker: tickCh,
|
||||
|
@ -199,20 +190,11 @@ func TestExecutorAutostopOK(t *testing.T) {
|
|||
})
|
||||
// Given: we have a user with a workspace
|
||||
workspace = mustProvisionWorkspace(t, client)
|
||||
ttl = time.Minute
|
||||
ttl = *workspace.TTL
|
||||
)
|
||||
// Given: workspace is running
|
||||
require.Equal(t, codersdk.WorkspaceTransitionStart, workspace.LatestBuild.Transition)
|
||||
|
||||
// Given: the workspace initially has autostop disabled
|
||||
require.Nil(t, workspace.TTL)
|
||||
|
||||
// When: we enable workspace autostop
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.UpdateWorkspaceTTL(ctx, workspace.ID, codersdk.UpdateWorkspaceTTLRequest{
|
||||
TTL: &ttl,
|
||||
}))
|
||||
|
||||
// When: the autobuild executor ticks *after* the TTL:
|
||||
go func() {
|
||||
tickCh <- time.Now().UTC().Add(ttl + time.Minute)
|
||||
|
@ -231,41 +213,32 @@ func TestExecutorAutostopAlreadyStopped(t *testing.T) {
|
|||
t.Parallel()
|
||||
|
||||
var (
|
||||
ctx = context.Background()
|
||||
err error
|
||||
tickCh = make(chan time.Time)
|
||||
client = coderdtest.New(t, &coderdtest.Options{
|
||||
AutobuildTicker: tickCh,
|
||||
IncludeProvisionerD: true,
|
||||
})
|
||||
// Given: we have a user with a workspace
|
||||
workspace = mustProvisionWorkspace(t, client)
|
||||
ttl = time.Minute
|
||||
// Given: we have a user with a workspace (disabling autostart)
|
||||
workspace = mustProvisionWorkspace(t, client, func(cwr *codersdk.CreateWorkspaceRequest) {
|
||||
cwr.AutostartSchedule = nil
|
||||
})
|
||||
ttl = *workspace.TTL
|
||||
)
|
||||
|
||||
// Given: workspace is stopped
|
||||
workspace = mustTransitionWorkspace(t, client, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
|
||||
|
||||
// Given: the workspace initially has autostop disabled
|
||||
require.Nil(t, workspace.TTL)
|
||||
|
||||
// When: we set the TTL on the workspace
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.UpdateWorkspaceTTL(ctx, workspace.ID, codersdk.UpdateWorkspaceTTLRequest{
|
||||
TTL: &ttl,
|
||||
}))
|
||||
|
||||
// When: the autobuild executor ticks past the TTL
|
||||
go func() {
|
||||
tickCh <- time.Now().UTC().Add(ttl)
|
||||
tickCh <- time.Now().UTC().Add(ttl + time.Minute)
|
||||
close(tickCh)
|
||||
}()
|
||||
|
||||
// Then: the workspace should not be stopped.
|
||||
<-time.After(5 * time.Second)
|
||||
ws := mustWorkspace(t, client, workspace.ID)
|
||||
require.Equal(t, workspace.LatestBuild.ID, ws.LatestBuild.ID, "expected no further workspace builds to occur")
|
||||
require.Equal(t, codersdk.WorkspaceTransitionStop, ws.LatestBuild.Transition, "expected workspace not to be running")
|
||||
require.Equal(t, workspace.LatestBuild.ID, ws.LatestBuild.ID, "expected no further workspace builds to occur")
|
||||
}
|
||||
|
||||
func TestExecutorAutostopNotEnabled(t *testing.T) {
|
||||
|
@ -277,17 +250,19 @@ func TestExecutorAutostopNotEnabled(t *testing.T) {
|
|||
AutobuildTicker: tickCh,
|
||||
IncludeProvisionerD: true,
|
||||
})
|
||||
// Given: we have a user with a workspace
|
||||
workspace = mustProvisionWorkspace(t, client)
|
||||
// Given: we have a user with a workspace that has no TTL set
|
||||
workspace = mustProvisionWorkspace(t, client, func(cwr *codersdk.CreateWorkspaceRequest) {
|
||||
cwr.TTL = nil
|
||||
})
|
||||
)
|
||||
|
||||
// Given: workspace has no TTL set
|
||||
require.Nil(t, workspace.TTL)
|
||||
|
||||
// Given: workspace is running
|
||||
require.Equal(t, codersdk.WorkspaceTransitionStart, workspace.LatestBuild.Transition)
|
||||
|
||||
// Given: the workspace has autostop disabled
|
||||
require.Empty(t, workspace.TTL)
|
||||
|
||||
// When: the autobuild executor ticks
|
||||
// When: the autobuild executor ticks past the TTL
|
||||
go func() {
|
||||
tickCh <- time.Now().UTC().Add(time.Minute)
|
||||
close(tickCh)
|
||||
|
@ -315,9 +290,6 @@ func TestExecutorWorkspaceDeleted(t *testing.T) {
|
|||
workspace = mustProvisionWorkspace(t, client)
|
||||
)
|
||||
|
||||
// Given: the workspace initially has autostart disabled
|
||||
require.Empty(t, workspace.AutostartSchedule)
|
||||
|
||||
// When: we enable workspace autostart
|
||||
sched, err := schedule.Weekly("* * * * *")
|
||||
require.NoError(t, err)
|
||||
|
@ -352,16 +324,15 @@ func TestExecutorWorkspaceAutostartTooEarly(t *testing.T) {
|
|||
AutobuildTicker: tickCh,
|
||||
IncludeProvisionerD: true,
|
||||
})
|
||||
// Given: we have a user with a workspace
|
||||
workspace = mustProvisionWorkspace(t, client)
|
||||
futureTime = time.Now().Add(time.Hour)
|
||||
futureTimeCron = fmt.Sprintf("%d %d * * *", futureTime.Minute(), futureTime.Hour())
|
||||
// Given: we have a user with a workspace configured to autostart some time in the future
|
||||
workspace = mustProvisionWorkspace(t, client, func(cwr *codersdk.CreateWorkspaceRequest) {
|
||||
cwr.AutostartSchedule = &futureTimeCron
|
||||
})
|
||||
)
|
||||
|
||||
// Given: the workspace initially has autostart disabled
|
||||
require.Empty(t, workspace.AutostartSchedule)
|
||||
|
||||
// When: we enable workspace autostart with some time in the future
|
||||
futureTime := time.Now().Add(time.Hour)
|
||||
futureTimeCron := fmt.Sprintf("%d %d * * *", futureTime.Minute(), futureTime.Hour())
|
||||
sched, err := schedule.Weekly(futureTimeCron)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.UpdateWorkspaceAutostart(ctx, workspace.ID, codersdk.UpdateWorkspaceAutostartRequest{
|
||||
|
@ -385,7 +356,6 @@ func TestExecutorWorkspaceTTLTooEarly(t *testing.T) {
|
|||
t.Parallel()
|
||||
|
||||
var (
|
||||
ctx = context.Background()
|
||||
tickCh = make(chan time.Time)
|
||||
client = coderdtest.New(t, &coderdtest.Options{
|
||||
AutobuildTicker: tickCh,
|
||||
|
@ -393,18 +363,9 @@ func TestExecutorWorkspaceTTLTooEarly(t *testing.T) {
|
|||
})
|
||||
// Given: we have a user with a workspace
|
||||
workspace = mustProvisionWorkspace(t, client)
|
||||
ttl = time.Hour
|
||||
)
|
||||
|
||||
// Given: the workspace initially has TTL unset
|
||||
require.Nil(t, workspace.TTL)
|
||||
|
||||
// When: we set the TTL to some time in the distant future
|
||||
require.NoError(t, client.UpdateWorkspaceTTL(ctx, workspace.ID, codersdk.UpdateWorkspaceTTLRequest{
|
||||
TTL: &ttl,
|
||||
}))
|
||||
|
||||
// When: the autobuild executor ticks
|
||||
// When: the autobuild executor ticks before the TTL
|
||||
go func() {
|
||||
tickCh <- time.Now().UTC()
|
||||
close(tickCh)
|
||||
|
@ -434,24 +395,15 @@ func TestExecutorAutostartMultipleOK(t *testing.T) {
|
|||
IncludeProvisionerD: true,
|
||||
})
|
||||
_ = coderdtest.New(t, &coderdtest.Options{
|
||||
AutobuildTicker: tickCh2,
|
||||
AutobuildTicker: tickCh2,
|
||||
IncludeProvisionerD: true,
|
||||
})
|
||||
// Given: we have a user with a workspace
|
||||
// Given: we have a user with a workspace that has autostart enabled (default)
|
||||
workspace = mustProvisionWorkspace(t, client)
|
||||
)
|
||||
// Given: workspace is stopped
|
||||
workspace = mustTransitionWorkspace(t, client, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
|
||||
|
||||
// Given: the workspace initially has autostart disabled
|
||||
require.Empty(t, workspace.AutostartSchedule)
|
||||
|
||||
// When: we enable workspace autostart
|
||||
sched, err := schedule.Weekly("* * * * *")
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.UpdateWorkspaceAutostart(ctx, workspace.ID, codersdk.UpdateWorkspaceAutostartRequest{
|
||||
Schedule: sched.String(),
|
||||
}))
|
||||
|
||||
// When: the autobuild executor ticks
|
||||
go func() {
|
||||
tickCh <- time.Now().UTC().Add(time.Minute)
|
||||
|
@ -479,13 +431,13 @@ func TestExecutorAutostartMultipleOK(t *testing.T) {
|
|||
require.True(t, builds[1].CreatedAt.After(builds[2].CreatedAt))
|
||||
}
|
||||
|
||||
func mustProvisionWorkspace(t *testing.T, client *codersdk.Client) codersdk.Workspace {
|
||||
func mustProvisionWorkspace(t *testing.T, client *codersdk.Client, mut ...func(*codersdk.CreateWorkspaceRequest)) codersdk.Workspace {
|
||||
t.Helper()
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||
ws := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||
ws := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID, mut...)
|
||||
coderdtest.AwaitWorkspaceBuildJob(t, client, ws.LatestBuild.ID)
|
||||
return mustWorkspace(t, client, ws.ID)
|
||||
}
|
||||
|
|
|
@ -55,6 +55,10 @@ func Weekly(raw string) (*Schedule, error) {
|
|||
return nil, xerrors.Errorf("expected *cron.SpecSchedule but got %T", specSched)
|
||||
}
|
||||
|
||||
if schedule.Location == time.Local {
|
||||
return nil, xerrors.Errorf("schedules scoped to time.Local are not supported")
|
||||
}
|
||||
|
||||
// Strip the leading CRON_TZ prefix so we just store the cron string.
|
||||
// The timezone info is available in SpecSchedule.
|
||||
cronStr := raw
|
||||
|
|
|
@ -41,6 +41,13 @@ func Test_Weekly(t *testing.T) {
|
|||
expectedTz: "UTC",
|
||||
expectedString: "CRON_TZ=UTC 30 9 * * 1-5",
|
||||
},
|
||||
{
|
||||
name: "time.Local will bite you",
|
||||
spec: "CRON_TZ=Local 30 9 * * 1-5",
|
||||
at: time.Time{},
|
||||
expectedNext: time.Time{},
|
||||
expectedError: "schedules scoped to time.Local are not supported",
|
||||
},
|
||||
{
|
||||
name: "invalid schedule",
|
||||
spec: "asdfasdfasdfsd",
|
||||
|
|
|
@ -389,11 +389,19 @@ func AwaitWorkspaceAgents(t *testing.T, client *codersdk.Client, build uuid.UUID
|
|||
|
||||
// CreateWorkspace creates a workspace for the user and template provided.
|
||||
// A random name is generated for it.
|
||||
func CreateWorkspace(t *testing.T, client *codersdk.Client, organization uuid.UUID, templateID uuid.UUID) codersdk.Workspace {
|
||||
workspace, err := client.CreateWorkspace(context.Background(), organization, codersdk.CreateWorkspaceRequest{
|
||||
TemplateID: templateID,
|
||||
Name: randomUsername(),
|
||||
})
|
||||
// To customize the defaults, pass a mutator func.
|
||||
func CreateWorkspace(t *testing.T, client *codersdk.Client, organization uuid.UUID, templateID uuid.UUID, mutators ...func(*codersdk.CreateWorkspaceRequest)) codersdk.Workspace {
|
||||
t.Helper()
|
||||
req := codersdk.CreateWorkspaceRequest{
|
||||
TemplateID: templateID,
|
||||
Name: randomUsername(),
|
||||
AutostartSchedule: ptr("CRON_TZ=US/Central * * * * *"),
|
||||
TTL: ptr(8 * time.Hour),
|
||||
}
|
||||
for _, mutator := range mutators {
|
||||
mutator(&req)
|
||||
}
|
||||
workspace, err := client.CreateWorkspace(context.Background(), organization, req)
|
||||
require.NoError(t, err)
|
||||
return workspace
|
||||
}
|
||||
|
@ -590,3 +598,7 @@ type roundTripper func(req *http.Request) (*http.Response, error)
|
|||
func (r roundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return r(req)
|
||||
}
|
||||
|
||||
func ptr[T any](x T) *T {
|
||||
return &x
|
||||
}
|
||||
|
|
|
@ -1456,13 +1456,15 @@ func (q *fakeQuerier) InsertWorkspace(_ context.Context, arg database.InsertWork
|
|||
|
||||
//nolint:gosimple
|
||||
workspace := database.Workspace{
|
||||
ID: arg.ID,
|
||||
CreatedAt: arg.CreatedAt,
|
||||
UpdatedAt: arg.UpdatedAt,
|
||||
OwnerID: arg.OwnerID,
|
||||
OrganizationID: arg.OrganizationID,
|
||||
TemplateID: arg.TemplateID,
|
||||
Name: arg.Name,
|
||||
ID: arg.ID,
|
||||
CreatedAt: arg.CreatedAt,
|
||||
UpdatedAt: arg.UpdatedAt,
|
||||
OwnerID: arg.OwnerID,
|
||||
OrganizationID: arg.OrganizationID,
|
||||
TemplateID: arg.TemplateID,
|
||||
Name: arg.Name,
|
||||
AutostartSchedule: arg.AutostartSchedule,
|
||||
Ttl: arg.Ttl,
|
||||
}
|
||||
q.workspaces = append(q.workspaces, workspace)
|
||||
return workspace, nil
|
||||
|
|
|
@ -3507,20 +3507,24 @@ INSERT INTO
|
|||
owner_id,
|
||||
organization_id,
|
||||
template_id,
|
||||
name
|
||||
name,
|
||||
autostart_schedule,
|
||||
ttl
|
||||
)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6, $7) RETURNING id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl
|
||||
`
|
||||
|
||||
type InsertWorkspaceParams struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
|
||||
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
|
||||
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
|
||||
Name string `db:"name" json:"name"`
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
|
||||
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
|
||||
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
|
||||
Name string `db:"name" json:"name"`
|
||||
AutostartSchedule sql.NullString `db:"autostart_schedule" json:"autostart_schedule"`
|
||||
Ttl sql.NullInt64 `db:"ttl" json:"ttl"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) InsertWorkspace(ctx context.Context, arg InsertWorkspaceParams) (Workspace, error) {
|
||||
|
@ -3532,6 +3536,8 @@ func (q *sqlQuerier) InsertWorkspace(ctx context.Context, arg InsertWorkspacePar
|
|||
arg.OrganizationID,
|
||||
arg.TemplateID,
|
||||
arg.Name,
|
||||
arg.AutostartSchedule,
|
||||
arg.Ttl,
|
||||
)
|
||||
var i Workspace
|
||||
err := row.Scan(
|
||||
|
|
|
@ -86,10 +86,12 @@ INSERT INTO
|
|||
owner_id,
|
||||
organization_id,
|
||||
template_id,
|
||||
name
|
||||
name,
|
||||
autostart_schedule,
|
||||
ttl
|
||||
)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6, $7) RETURNING *;
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *;
|
||||
|
||||
-- name: UpdateWorkspaceDeletedByID :exec
|
||||
UPDATE
|
||||
|
|
|
@ -357,6 +357,25 @@ func (api *api) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
|
|||
return
|
||||
}
|
||||
|
||||
var dbAutostartSchedule sql.NullString
|
||||
if createWorkspace.AutostartSchedule != nil {
|
||||
_, err := schedule.Weekly(*createWorkspace.AutostartSchedule)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
|
||||
Message: fmt.Sprintf("parse autostart schedule: %s", err.Error()),
|
||||
})
|
||||
return
|
||||
}
|
||||
dbAutostartSchedule.Valid = true
|
||||
dbAutostartSchedule.String = *createWorkspace.AutostartSchedule
|
||||
}
|
||||
|
||||
var dbTTL sql.NullInt64
|
||||
if createWorkspace.TTL != nil && *createWorkspace.TTL > 0 {
|
||||
dbTTL.Valid = true
|
||||
dbTTL.Int64 = int64(*createWorkspace.TTL)
|
||||
}
|
||||
|
||||
workspace, err := api.Database.GetWorkspaceByOwnerIDAndName(r.Context(), database.GetWorkspaceByOwnerIDAndNameParams{
|
||||
OwnerID: apiKey.UserID,
|
||||
Name: createWorkspace.Name,
|
||||
|
@ -423,16 +442,19 @@ func (api *api) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
|
|||
var provisionerJob database.ProvisionerJob
|
||||
var workspaceBuild database.WorkspaceBuild
|
||||
err = api.Database.InTx(func(db database.Store) error {
|
||||
now := database.Now()
|
||||
workspaceBuildID := uuid.New()
|
||||
// Workspaces are created without any versions.
|
||||
workspace, err = db.InsertWorkspace(r.Context(), database.InsertWorkspaceParams{
|
||||
ID: uuid.New(),
|
||||
CreatedAt: database.Now(),
|
||||
UpdatedAt: database.Now(),
|
||||
OwnerID: apiKey.UserID,
|
||||
OrganizationID: template.OrganizationID,
|
||||
TemplateID: template.ID,
|
||||
Name: createWorkspace.Name,
|
||||
ID: uuid.New(),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
OwnerID: apiKey.UserID,
|
||||
OrganizationID: template.OrganizationID,
|
||||
TemplateID: template.ID,
|
||||
Name: createWorkspace.Name,
|
||||
AutostartSchedule: dbAutostartSchedule,
|
||||
Ttl: dbTTL,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("insert workspace: %w", err)
|
||||
|
@ -441,8 +463,8 @@ func (api *api) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
|
|||
_, err = db.InsertParameterValue(r.Context(), database.InsertParameterValueParams{
|
||||
ID: uuid.New(),
|
||||
Name: parameterValue.Name,
|
||||
CreatedAt: database.Now(),
|
||||
UpdatedAt: database.Now(),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
Scope: database.ParameterScopeWorkspace,
|
||||
ScopeID: workspace.ID,
|
||||
SourceScheme: database.ParameterSourceScheme(parameterValue.SourceScheme),
|
||||
|
@ -462,8 +484,8 @@ func (api *api) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
|
|||
}
|
||||
provisionerJob, err = db.InsertProvisionerJob(r.Context(), database.InsertProvisionerJobParams{
|
||||
ID: uuid.New(),
|
||||
CreatedAt: database.Now(),
|
||||
UpdatedAt: database.Now(),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
InitiatorID: apiKey.UserID,
|
||||
OrganizationID: template.OrganizationID,
|
||||
Provisioner: template.Provisioner,
|
||||
|
@ -477,8 +499,8 @@ func (api *api) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
|
|||
}
|
||||
workspaceBuild, err = db.InsertWorkspaceBuild(r.Context(), database.InsertWorkspaceBuildParams{
|
||||
ID: workspaceBuildID,
|
||||
CreatedAt: database.Now(),
|
||||
UpdatedAt: database.Now(),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
WorkspaceID: workspace.ID,
|
||||
TemplateVersionID: templateVersion.ID,
|
||||
Name: namesgenerator.GetRandomName(1),
|
||||
|
|
|
@ -481,7 +481,10 @@ func TestWorkspaceUpdateAutostart(t *testing.T) {
|
|||
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||
project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID)
|
||||
workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
|
||||
cwr.AutostartSchedule = nil
|
||||
cwr.TTL = nil
|
||||
})
|
||||
)
|
||||
|
||||
// ensure test invariant: new workspaces have no autostart schedule.
|
||||
|
@ -566,7 +569,10 @@ func TestWorkspaceUpdateAutostop(t *testing.T) {
|
|||
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||
project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID)
|
||||
workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
|
||||
cwr.AutostartSchedule = nil
|
||||
cwr.TTL = nil
|
||||
})
|
||||
)
|
||||
|
||||
// ensure test invariant: new workspaces have no autostop schedule.
|
||||
|
|
|
@ -65,8 +65,10 @@ type CreateTemplateRequest struct {
|
|||
|
||||
// CreateWorkspaceRequest provides options for creating a new workspace.
|
||||
type CreateWorkspaceRequest struct {
|
||||
TemplateID uuid.UUID `json:"template_id" validate:"required"`
|
||||
Name string `json:"name" validate:"username,required"`
|
||||
TemplateID uuid.UUID `json:"template_id" validate:"required"`
|
||||
Name string `json:"name" validate:"username,required"`
|
||||
AutostartSchedule *string `json:"autostart_schedule"`
|
||||
TTL *time.Duration `json:"ttl"`
|
||||
// ParameterValues allows for additional parameters to be provided
|
||||
// during the initial provision.
|
||||
ParameterValues []CreateParameterRequest `json:"parameter_values,omitempty"`
|
||||
|
|
|
@ -94,6 +94,9 @@ export interface CreateWorkspaceBuildRequest {
|
|||
export interface CreateWorkspaceRequest {
|
||||
readonly template_id: string
|
||||
readonly name: string
|
||||
readonly autostart_schedule?: string
|
||||
// This is likely an enum in an external package ("time.Duration")
|
||||
readonly ttl?: number
|
||||
readonly parameter_values?: CreateParameterRequest[]
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue