mirror of
https://github.com/coder/coder.git
synced 2025-07-03 16:13:58 +00:00
coderd: autostart: codersdk, http api, database plumbing (#879)
* feat: add columns autostart_schedule, autostop_schedule to database schema * feat: database: add UpdateWorkspaceAutostart and UpdateWorkspaceAutostop methods * feat: add AutostartSchedule/AutostopSchedule to api workspace struct * feat: codersdk: implement update workspace autostart and autostop methods * chore: add unit tests for workspace autostarat and autostop methods
This commit is contained in:
@ -2,12 +2,15 @@ package coderd_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/coderd/autostart/schedule"
|
||||
"github.com/coder/coder/coderd/coderdtest"
|
||||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/codersdk"
|
||||
@ -182,3 +185,270 @@ func TestWorkspaceBuildByName(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestWorkspaceUpdateAutostart(t *testing.T) {
|
||||
t.Parallel()
|
||||
var dublinLoc = mustLocation(t, "Europe/Dublin")
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
schedule string
|
||||
expectedError string
|
||||
at time.Time
|
||||
expectedNext time.Time
|
||||
expectedInterval time.Duration
|
||||
}{
|
||||
{
|
||||
name: "disable autostart",
|
||||
schedule: "",
|
||||
expectedError: "",
|
||||
},
|
||||
{
|
||||
name: "friday to monday",
|
||||
schedule: "CRON_TZ=Europe/Dublin 30 9 1-5",
|
||||
expectedError: "",
|
||||
at: time.Date(2022, 5, 6, 9, 31, 0, 0, dublinLoc),
|
||||
expectedNext: time.Date(2022, 5, 9, 9, 30, 0, 0, dublinLoc),
|
||||
expectedInterval: 71*time.Hour + 59*time.Minute,
|
||||
},
|
||||
{
|
||||
name: "monday to tuesday",
|
||||
schedule: "CRON_TZ=Europe/Dublin 30 9 1-5",
|
||||
expectedError: "",
|
||||
at: time.Date(2022, 5, 9, 9, 31, 0, 0, dublinLoc),
|
||||
expectedNext: time.Date(2022, 5, 10, 9, 30, 0, 0, dublinLoc),
|
||||
expectedInterval: 23*time.Hour + 59*time.Minute,
|
||||
},
|
||||
{
|
||||
// DST in Ireland began on Mar 27 in 2022 at 0100. Forward 1 hour.
|
||||
name: "DST start",
|
||||
schedule: "CRON_TZ=Europe/Dublin 30 9 *",
|
||||
expectedError: "",
|
||||
at: time.Date(2022, 3, 26, 9, 31, 0, 0, dublinLoc),
|
||||
expectedNext: time.Date(2022, 3, 27, 9, 30, 0, 0, dublinLoc),
|
||||
expectedInterval: 22*time.Hour + 59*time.Minute,
|
||||
},
|
||||
{
|
||||
// DST in Ireland ends on Oct 30 in 2022 at 0200. Back 1 hour.
|
||||
name: "DST end",
|
||||
schedule: "CRON_TZ=Europe/Dublin 30 9 *",
|
||||
expectedError: "",
|
||||
at: time.Date(2022, 10, 29, 9, 31, 0, 0, dublinLoc),
|
||||
expectedNext: time.Date(2022, 10, 30, 9, 30, 0, 0, dublinLoc),
|
||||
expectedInterval: 24*time.Hour + 59*time.Minute,
|
||||
},
|
||||
{
|
||||
name: "invalid location",
|
||||
schedule: "CRON_TZ=Imaginary/Place 30 9 1-5",
|
||||
expectedError: "status code 500: invalid autostart schedule: parse schedule: provided bad location Imaginary/Place: unknown time zone Imaginary/Place",
|
||||
},
|
||||
{
|
||||
name: "invalid schedule",
|
||||
schedule: "asdf asdf asdf ",
|
||||
expectedError: `status code 500: invalid autostart schedule: parse schedule: failed to parse int from asdf: strconv.Atoi: parsing "asdf": invalid syntax`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
var (
|
||||
ctx = context.Background()
|
||||
client = coderdtest.New(t, nil)
|
||||
_ = coderdtest.NewProvisionerDaemon(t, client)
|
||||
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)
|
||||
workspace = coderdtest.CreateWorkspace(t, client, codersdk.Me, project.ID)
|
||||
)
|
||||
|
||||
// ensure test invariant: new workspaces have no autostart schedule.
|
||||
require.Empty(t, workspace.AutostartSchedule, "expected newly-minted workspace to have no autostart schedule")
|
||||
|
||||
err := client.UpdateWorkspaceAutostart(ctx, workspace.ID, codersdk.UpdateWorkspaceAutostartRequest{
|
||||
Schedule: testCase.schedule,
|
||||
})
|
||||
|
||||
if testCase.expectedError != "" {
|
||||
require.EqualError(t, err, testCase.expectedError, "unexpected error when setting workspace autostart schedule")
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err, "expected no error setting workspace autostart schedule")
|
||||
|
||||
updated, err := client.Workspace(ctx, workspace.ID)
|
||||
require.NoError(t, err, "fetch updated workspace")
|
||||
|
||||
require.Equal(t, testCase.schedule, updated.AutostartSchedule, "expected autostart schedule to equal requested")
|
||||
|
||||
if testCase.schedule == "" {
|
||||
return
|
||||
}
|
||||
sched, err := schedule.Weekly(updated.AutostartSchedule)
|
||||
require.NoError(t, err, "parse returned schedule")
|
||||
|
||||
next := sched.Next(testCase.at)
|
||||
require.Equal(t, testCase.expectedNext, next, "unexpected next scheduled autostart time")
|
||||
interval := next.Sub(testCase.at)
|
||||
require.Equal(t, testCase.expectedInterval, interval, "unexpected interval")
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("NotFound", func(t *testing.T) {
|
||||
var (
|
||||
ctx = context.Background()
|
||||
client = coderdtest.New(t, nil)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
wsid = uuid.New()
|
||||
req = codersdk.UpdateWorkspaceAutostartRequest{
|
||||
Schedule: "9 30 1-5",
|
||||
}
|
||||
)
|
||||
|
||||
err := client.UpdateWorkspaceAutostart(ctx, wsid, req)
|
||||
require.IsType(t, err, &codersdk.Error{}, "expected codersdk.Error")
|
||||
coderSDKErr, _ := err.(*codersdk.Error) //nolint:errorlint
|
||||
require.Equal(t, coderSDKErr.StatusCode(), 404, "expected status code 404")
|
||||
require.Equal(t, fmt.Sprintf("workspace %q does not exist", wsid), coderSDKErr.Message, "unexpected response code")
|
||||
})
|
||||
}
|
||||
|
||||
func TestWorkspaceUpdateAutostop(t *testing.T) {
|
||||
t.Parallel()
|
||||
var dublinLoc = mustLocation(t, "Europe/Dublin")
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
schedule string
|
||||
expectedError string
|
||||
at time.Time
|
||||
expectedNext time.Time
|
||||
expectedInterval time.Duration
|
||||
}{
|
||||
{
|
||||
name: "disable autostop",
|
||||
schedule: "",
|
||||
expectedError: "",
|
||||
},
|
||||
{
|
||||
name: "friday to monday",
|
||||
schedule: "CRON_TZ=Europe/Dublin 30 17 1-5",
|
||||
expectedError: "",
|
||||
at: time.Date(2022, 5, 6, 17, 31, 0, 0, dublinLoc),
|
||||
expectedNext: time.Date(2022, 5, 9, 17, 30, 0, 0, dublinLoc),
|
||||
expectedInterval: 71*time.Hour + 59*time.Minute,
|
||||
},
|
||||
{
|
||||
name: "monday to tuesday",
|
||||
schedule: "CRON_TZ=Europe/Dublin 30 17 1-5",
|
||||
expectedError: "",
|
||||
at: time.Date(2022, 5, 9, 17, 31, 0, 0, dublinLoc),
|
||||
expectedNext: time.Date(2022, 5, 10, 17, 30, 0, 0, dublinLoc),
|
||||
expectedInterval: 23*time.Hour + 59*time.Minute,
|
||||
},
|
||||
{
|
||||
// DST in Ireland began on Mar 27 in 2022 at 0100. Forward 1 hour.
|
||||
name: "DST start",
|
||||
schedule: "CRON_TZ=Europe/Dublin 30 17 *",
|
||||
expectedError: "",
|
||||
at: time.Date(2022, 3, 26, 17, 31, 0, 0, dublinLoc),
|
||||
expectedNext: time.Date(2022, 3, 27, 17, 30, 0, 0, dublinLoc),
|
||||
expectedInterval: 22*time.Hour + 59*time.Minute,
|
||||
},
|
||||
{
|
||||
// DST in Ireland ends on Oct 30 in 2022 at 0200. Back 1 hour.
|
||||
name: "DST end",
|
||||
schedule: "CRON_TZ=Europe/Dublin 30 17 *",
|
||||
expectedError: "",
|
||||
at: time.Date(2022, 10, 29, 17, 31, 0, 0, dublinLoc),
|
||||
expectedNext: time.Date(2022, 10, 30, 17, 30, 0, 0, dublinLoc),
|
||||
expectedInterval: 24*time.Hour + 59*time.Minute,
|
||||
},
|
||||
{
|
||||
name: "invalid location",
|
||||
schedule: "CRON_TZ=Imaginary/Place 30 17 1-5",
|
||||
expectedError: "status code 500: invalid autostop schedule: parse schedule: provided bad location Imaginary/Place: unknown time zone Imaginary/Place",
|
||||
},
|
||||
{
|
||||
name: "invalid schedule",
|
||||
schedule: "asdf asdf asdf ",
|
||||
expectedError: `status code 500: invalid autostop schedule: parse schedule: failed to parse int from asdf: strconv.Atoi: parsing "asdf": invalid syntax`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
var (
|
||||
ctx = context.Background()
|
||||
client = coderdtest.New(t, nil)
|
||||
_ = coderdtest.NewProvisionerDaemon(t, client)
|
||||
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)
|
||||
workspace = coderdtest.CreateWorkspace(t, client, codersdk.Me, project.ID)
|
||||
)
|
||||
|
||||
// ensure test invariant: new workspaces have no autostop schedule.
|
||||
require.Empty(t, workspace.AutostopSchedule, "expected newly-minted workspace to have no autstop schedule")
|
||||
|
||||
err := client.UpdateWorkspaceAutostop(ctx, workspace.ID, codersdk.UpdateWorkspaceAutostopRequest{
|
||||
Schedule: testCase.schedule,
|
||||
})
|
||||
|
||||
if testCase.expectedError != "" {
|
||||
require.EqualError(t, err, testCase.expectedError, "unexpected error when setting workspace autostop schedule")
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err, "expected no error setting workspace autostop schedule")
|
||||
|
||||
updated, err := client.Workspace(ctx, workspace.ID)
|
||||
require.NoError(t, err, "fetch updated workspace")
|
||||
|
||||
require.Equal(t, testCase.schedule, updated.AutostopSchedule, "expected autostop schedule to equal requested")
|
||||
|
||||
if testCase.schedule == "" {
|
||||
return
|
||||
}
|
||||
sched, err := schedule.Weekly(updated.AutostopSchedule)
|
||||
require.NoError(t, err, "parse returned schedule")
|
||||
|
||||
next := sched.Next(testCase.at)
|
||||
require.Equal(t, testCase.expectedNext, next, "unexpected next scheduled autostop time")
|
||||
interval := next.Sub(testCase.at)
|
||||
require.Equal(t, testCase.expectedInterval, interval, "unexpected interval")
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("NotFound", func(t *testing.T) {
|
||||
var (
|
||||
ctx = context.Background()
|
||||
client = coderdtest.New(t, nil)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
wsid = uuid.New()
|
||||
req = codersdk.UpdateWorkspaceAutostopRequest{
|
||||
Schedule: "9 30 1-5",
|
||||
}
|
||||
)
|
||||
|
||||
err := client.UpdateWorkspaceAutostop(ctx, wsid, req)
|
||||
require.IsType(t, err, &codersdk.Error{}, "expected codersdk.Error")
|
||||
coderSDKErr, _ := err.(*codersdk.Error) //nolint:errorlint
|
||||
require.Equal(t, coderSDKErr.StatusCode(), 404, "expected status code 404")
|
||||
require.Equal(t, fmt.Sprintf("workspace %q does not exist", wsid), coderSDKErr.Message, "unexpected response code")
|
||||
})
|
||||
}
|
||||
|
||||
func mustLocation(t *testing.T, location string) *time.Location {
|
||||
loc, err := time.LoadLocation(location)
|
||||
if err != nil {
|
||||
t.Errorf("failed to load location %s: %s", location, err.Error())
|
||||
}
|
||||
|
||||
return loc
|
||||
}
|
||||
|
Reference in New Issue
Block a user