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

Skip to content

Commit be0e6f3

Browse files
committed
feat: implement autostop database/http/api plumbing
1 parent 4a7e6eb commit be0e6f3

File tree

4 files changed

+183
-4
lines changed

4 files changed

+183
-4
lines changed

coderd/coderd.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,9 @@ func New(options *Options) (http.Handler, func()) {
187187
r.Route("/autostart", func(r chi.Router) {
188188
r.Put("/", api.putWorkspaceAutostart)
189189
})
190+
r.Route("/autostop", func(r chi.Router) {
191+
r.Put("/", api.putWorkspaceAutostop)
192+
})
190193
})
191194
r.Route("/workspacebuilds/{workspacebuild}", func(r chi.Router) {
192195
r.Use(

coderd/workspaces.go

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,37 @@ func (api *api) putWorkspaceAutostart(rw http.ResponseWriter, r *http.Request) {
323323
}
324324
}
325325

326-
// TODO(cian): api.updateWorkspaceAutostop
326+
func (api *api) putWorkspaceAutostop(rw http.ResponseWriter, r *http.Request) {
327+
var req codersdk.UpdateWorkspaceAutostopRequest
328+
if !httpapi.Read(rw, r, &req) {
329+
return
330+
}
331+
332+
var dbSched sql.NullString
333+
if req.Schedule != "" {
334+
validSched, err := schedule.Weekly(req.Schedule)
335+
if err != nil {
336+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
337+
Message: fmt.Sprintf("invalid autostop schedule: %s", err),
338+
})
339+
return
340+
}
341+
dbSched.String = validSched.String()
342+
dbSched.Valid = true
343+
}
344+
345+
workspace := httpmw.WorkspaceParam(r)
346+
err := api.Database.UpdateWorkspaceAutostop(r.Context(), database.UpdateWorkspaceAutostopParams{
347+
ID: workspace.ID,
348+
AutostopSchedule: dbSched,
349+
})
350+
if err != nil {
351+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
352+
Message: fmt.Sprintf("update workspace autostop schedule: %s", err),
353+
})
354+
return
355+
}
356+
}
327357

328358
func convertWorkspace(workspace database.Workspace, workspaceBuild codersdk.WorkspaceBuild, template database.Template) codersdk.Workspace {
329359
return codersdk.Workspace{

coderd/workspaces_test.go

Lines changed: 130 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,136 @@ func TestWorkspaceUpdateAutostart(t *testing.T) {
309309

310310
err := client.UpdateWorkspaceAutostart(ctx, wsid, req)
311311
require.IsType(t, err, &codersdk.Error{}, "expected codersdk.Error")
312-
coderSDKErr, _ := err.(*codersdk.Error)
312+
coderSDKErr, _ := err.(*codersdk.Error) //nolint:errorlint
313+
require.Equal(t, coderSDKErr.StatusCode(), 404, "expected status code 404")
314+
require.Equal(t, fmt.Sprintf("workspace %q does not exist", wsid), coderSDKErr.Message, "unexpected response code")
315+
})
316+
}
317+
318+
func TestWorkspaceUpdateAutostop(t *testing.T) {
319+
t.Parallel()
320+
var dublinLoc = mustLocation(t, "Europe/Dublin")
321+
322+
testCases := []struct {
323+
name string
324+
schedule string
325+
expectedError string
326+
at time.Time
327+
expectedNext time.Time
328+
expectedInterval time.Duration
329+
}{
330+
{
331+
name: "disable autostop",
332+
schedule: "",
333+
expectedError: "",
334+
},
335+
{
336+
name: "friday to monday",
337+
schedule: "CRON_TZ=Europe/Dublin 30 17 1-5",
338+
expectedError: "",
339+
at: time.Date(2022, 5, 6, 17, 31, 0, 0, dublinLoc),
340+
expectedNext: time.Date(2022, 5, 9, 17, 30, 0, 0, dublinLoc),
341+
expectedInterval: 71*time.Hour + 59*time.Minute,
342+
},
343+
{
344+
name: "monday to tuesday",
345+
schedule: "CRON_TZ=Europe/Dublin 30 17 1-5",
346+
expectedError: "",
347+
at: time.Date(2022, 5, 9, 17, 31, 0, 0, dublinLoc),
348+
expectedNext: time.Date(2022, 5, 10, 17, 30, 0, 0, dublinLoc),
349+
expectedInterval: 23*time.Hour + 59*time.Minute,
350+
},
351+
{
352+
// DST in Ireland began on Mar 27 in 2022 at 0100. Forward 1 hour.
353+
name: "DST start",
354+
schedule: "CRON_TZ=Europe/Dublin 30 17 *",
355+
expectedError: "",
356+
at: time.Date(2022, 3, 26, 17, 31, 0, 0, dublinLoc),
357+
expectedNext: time.Date(2022, 3, 27, 17, 30, 0, 0, dublinLoc),
358+
expectedInterval: 22*time.Hour + 59*time.Minute,
359+
},
360+
{
361+
// DST in Ireland ends on Oct 30 in 2022 at 0200. Back 1 hour.
362+
name: "DST end",
363+
schedule: "CRON_TZ=Europe/Dublin 30 17 *",
364+
expectedError: "",
365+
at: time.Date(2022, 10, 29, 17, 31, 0, 0, dublinLoc),
366+
expectedNext: time.Date(2022, 10, 30, 17, 30, 0, 0, dublinLoc),
367+
expectedInterval: 24*time.Hour + 59*time.Minute,
368+
},
369+
{
370+
name: "invalid location",
371+
schedule: "CRON_TZ=Imaginary/Place 30 17 1-5",
372+
expectedError: "status code 500: invalid autostop schedule: parse schedule: provided bad location Imaginary/Place: unknown time zone Imaginary/Place",
373+
},
374+
{
375+
name: "invalid schedule",
376+
schedule: "asdf asdf asdf ",
377+
expectedError: `status code 500: invalid autostop schedule: parse schedule: failed to parse int from asdf: strconv.Atoi: parsing "asdf": invalid syntax`,
378+
},
379+
}
380+
381+
for _, testCase := range testCases {
382+
testCase := testCase
383+
t.Run(testCase.name, func(t *testing.T) {
384+
t.Parallel()
385+
var (
386+
ctx = context.Background()
387+
client = coderdtest.New(t, nil)
388+
_ = coderdtest.NewProvisionerDaemon(t, client)
389+
user = coderdtest.CreateFirstUser(t, client)
390+
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
391+
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
392+
project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
393+
workspace = coderdtest.CreateWorkspace(t, client, codersdk.Me, project.ID)
394+
)
395+
396+
// ensure test invariant: new workspaces have no autostop schedule.
397+
require.Empty(t, workspace.AutostopSchedule, "expected newly-minted workspace to have no autstop schedule")
398+
399+
err := client.UpdateWorkspaceAutostop(ctx, workspace.ID, codersdk.UpdateWorkspaceAutostopRequest{
400+
Schedule: testCase.schedule,
401+
})
402+
403+
if testCase.expectedError != "" {
404+
require.EqualError(t, err, testCase.expectedError, "unexpected error when setting workspace autostop schedule")
405+
return
406+
}
407+
408+
require.NoError(t, err, "expected no error setting workspace autostop schedule")
409+
410+
updated, err := client.Workspace(ctx, workspace.ID)
411+
require.NoError(t, err, "fetch updated workspace")
412+
413+
require.Equal(t, testCase.schedule, updated.AutostopSchedule, "expected autostop schedule to equal requested")
414+
415+
if testCase.schedule == "" {
416+
return
417+
}
418+
sched, err := schedule.Weekly(updated.AutostopSchedule)
419+
require.NoError(t, err, "parse returned schedule")
420+
421+
next := sched.Next(testCase.at)
422+
require.Equal(t, testCase.expectedNext, next, "unexpected next scheduled autostop time")
423+
interval := next.Sub(testCase.at)
424+
require.Equal(t, testCase.expectedInterval, interval, "unexpected interval")
425+
})
426+
}
427+
428+
t.Run("NotFound", func(t *testing.T) {
429+
var (
430+
ctx = context.Background()
431+
client = coderdtest.New(t, nil)
432+
_ = coderdtest.CreateFirstUser(t, client)
433+
wsid = uuid.New()
434+
req = codersdk.UpdateWorkspaceAutostopRequest{
435+
Schedule: "9 30 1-5",
436+
}
437+
)
438+
439+
err := client.UpdateWorkspaceAutostop(ctx, wsid, req)
440+
require.IsType(t, err, &codersdk.Error{}, "expected codersdk.Error")
441+
coderSDKErr, _ := err.(*codersdk.Error) //nolint:errorlint
313442
require.Equal(t, coderSDKErr.StatusCode(), 404, "expected status code 404")
314443
require.Equal(t, fmt.Sprintf("workspace %q does not exist", wsid), coderSDKErr.Message, "unexpected response code")
315444
})

codersdk/workspaces.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,25 @@ func (c *Client) UpdateWorkspaceAutostart(ctx context.Context, id uuid.UUID, req
107107
if res.StatusCode != http.StatusOK {
108108
return readBodyAsError(res)
109109
}
110-
// TODO(cian): should we return the updated schedule?
111110
return nil
112111
}
113112

114-
// TODO(cian): client.UpdateWorkspaceAutostop
113+
// UpdateWorkspaceAutostopRequest is a request to update a workspace's autostop schedule.
114+
type UpdateWorkspaceAutostopRequest struct {
115+
Schedule string
116+
}
117+
118+
// UpdateWorkspaceAutostop sets the autostop schedule for workspace by id.
119+
// If the provided schedule is empty, autostop is disabled for the workspace.
120+
func (c *Client) UpdateWorkspaceAutostop(ctx context.Context, id uuid.UUID, req UpdateWorkspaceAutostopRequest) error {
121+
path := fmt.Sprintf("/api/v2/workspaces/%s/autostop", id.String())
122+
res, err := c.request(ctx, http.MethodPut, path, req)
123+
if err != nil {
124+
return xerrors.Errorf("update workspace autostop: %w", err)
125+
}
126+
defer res.Body.Close()
127+
if res.StatusCode != http.StatusOK {
128+
return readBodyAsError(res)
129+
}
130+
return nil
131+
}

0 commit comments

Comments
 (0)