diff --git a/coderd/autobuild/lifecycle_executor_test.go b/coderd/autobuild/lifecycle_executor_test.go index 3eb779376cc5c..b271fc43d1267 100644 --- a/coderd/autobuild/lifecycle_executor_test.go +++ b/coderd/autobuild/lifecycle_executor_test.go @@ -722,45 +722,17 @@ func TestExecutorWorkspaceAutostopNoWaitChangedMyMind(t *testing.T) { err := client.UpdateWorkspaceTTL(ctx, workspace.ID, codersdk.UpdateWorkspaceTTLRequest{TTLMillis: nil}) require.NoError(t, err) - // Then: the deadline should still be the original value + // Then: the deadline should be set to zero updated := coderdtest.MustWorkspace(t, client, workspace.ID) - assert.WithinDuration(t, workspace.LatestBuild.Deadline.Time, updated.LatestBuild.Deadline.Time, time.Minute) + assert.True(t, !updated.LatestBuild.Deadline.Valid) // When: the autobuild executor ticks after the original deadline go func() { tickCh <- workspace.LatestBuild.Deadline.Time.Add(time.Minute) }() - // Then: the workspace should stop - stats := <-statsCh - assert.Len(t, stats.Errors, 0) - assert.Len(t, stats.Transitions, 1) - assert.Equal(t, stats.Transitions[workspace.ID], database.WorkspaceTransitionStop) - - // Wait for stop to complete - updated = coderdtest.MustWorkspace(t, client, workspace.ID) - _ = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, updated.LatestBuild.ID) - - // Start the workspace again - workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID, database.WorkspaceTransitionStop, database.WorkspaceTransitionStart) - - // Given: the user changes their mind again and wants to enable autostop - newTTL := 8 * time.Hour - err = client.UpdateWorkspaceTTL(ctx, workspace.ID, codersdk.UpdateWorkspaceTTLRequest{TTLMillis: ptr.Ref(newTTL.Milliseconds())}) - require.NoError(t, err) - - // Then: the deadline should remain at the zero value - updated = coderdtest.MustWorkspace(t, client, workspace.ID) - assert.Zero(t, updated.LatestBuild.Deadline) - - // When: the relentless onward march of time continues - go func() { - tickCh <- workspace.LatestBuild.Deadline.Time.Add(newTTL + time.Minute) - close(tickCh) - }() - // Then: the workspace should not stop - stats = <-statsCh + stats := <-statsCh assert.Len(t, stats.Errors, 0) assert.Len(t, stats.Transitions, 0) } diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 19fb1ec1ce810..0e7a4b5972dfd 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -1029,6 +1029,26 @@ func (api *API) putWorkspaceTTL(rw http.ResponseWriter, r *http.Request) { return xerrors.Errorf("update workspace time until shutdown: %w", err) } + // If autostop has been disabled, we want to remove the deadline from the + // existing workspace build (if there is one). + if !dbTTL.Valid { + build, err := s.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID) + if err != nil { + return xerrors.Errorf("get latest workspace build: %w", err) + } + + if build.Transition == database.WorkspaceTransitionStart { + if err = s.UpdateWorkspaceBuildDeadlineByID(ctx, database.UpdateWorkspaceBuildDeadlineByIDParams{ + ID: build.ID, + Deadline: time.Time{}, + MaxDeadline: build.MaxDeadline, + UpdatedAt: dbtime.Time(api.Clock.Now()), + }); err != nil { + return xerrors.Errorf("update workspace build deadline: %w", err) + } + } + } + return nil }, nil) if err != nil { diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index d6e365011b929..115549e28cc2b 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -2394,6 +2394,93 @@ func TestWorkspaceUpdateTTL(t *testing.T) { }) } + t.Run("ModifyAutostopWithRunningWorkspace", func(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + fromTTL *int64 + toTTL *int64 + afterUpdate func(t *testing.T, before, after codersdk.NullTime) + }{ + { + name: "RemoveAutostopRemovesDeadline", + fromTTL: ptr.Ref((8 * time.Hour).Milliseconds()), + toTTL: nil, + afterUpdate: func(t *testing.T, before, after codersdk.NullTime) { + require.NotZero(t, before) + require.Zero(t, after) + }, + }, + { + name: "AddAutostopDoesNotAddDeadline", + fromTTL: nil, + toTTL: ptr.Ref((8 * time.Hour).Milliseconds()), + afterUpdate: func(t *testing.T, before, after codersdk.NullTime) { + require.Zero(t, before) + require.Zero(t, after) + }, + }, + { + name: "IncreaseAutostopDoesNotModifyDeadline", + fromTTL: ptr.Ref((4 * time.Hour).Milliseconds()), + toTTL: ptr.Ref((8 * time.Hour).Milliseconds()), + afterUpdate: func(t *testing.T, before, after codersdk.NullTime) { + require.NotZero(t, before) + require.NotZero(t, after) + require.Equal(t, before, after) + }, + }, + { + name: "DecreaseAutostopDoesNotModifyDeadline", + fromTTL: ptr.Ref((8 * time.Hour).Milliseconds()), + toTTL: ptr.Ref((4 * time.Hour).Milliseconds()), + afterUpdate: func(t *testing.T, before, after codersdk.NullTime) { + require.NotZero(t, before) + require.NotZero(t, after) + require.Equal(t, before, after) + }, + }, + } + + for _, testCase := range testCases { + testCase := testCase + + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + var ( + client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + user = coderdtest.CreateFirstUser(t, client) + version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + _ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + template = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + workspace = coderdtest.CreateWorkspace(t, client, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) { + cwr.TTLMillis = testCase.fromTTL + }) + build = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID) + ) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + err := client.UpdateWorkspaceTTL(ctx, workspace.ID, codersdk.UpdateWorkspaceTTLRequest{ + TTLMillis: testCase.toTTL, + }) + require.NoError(t, err) + + deadlineBefore := build.Deadline + + build, err = client.WorkspaceBuild(ctx, build.ID) + require.NoError(t, err) + + deadlineAfter := build.Deadline + + testCase.afterUpdate(t, deadlineBefore, deadlineAfter) + }) + } + }) + t.Run("CustomAutostopDisabledByTemplate", func(t *testing.T) { t.Parallel() var ( diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx index b5e9bfdba8da6..4ee96204dbdd5 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx @@ -118,7 +118,10 @@ export const WorkspaceSchedulePage: FC = () => { await submitScheduleMutation.mutateAsync(data); - if (data.autostopChanged) { + if ( + data.autostopChanged && + getAutostop(workspace).autostopEnabled + ) { setIsConfirmingApply(true); } }}