feat: allow removing deadline for running workspace (#16085)

Fixes https://github.com/coder/coder/issues/9775

When a workspace's TTL is removed, and the workspace is running, the
deadline is removed from the workspace.

This also modifies the frontend to not show a confirmation dialog when
the change is to remove autostop.
This commit is contained in:
Danielle Maywood
2025-01-13 21:37:57 +00:00
committed by GitHub
parent 048a10a318
commit 7c595e2631
4 changed files with 114 additions and 32 deletions
+3 -31
View File
@@ -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)
}
+20
View File
@@ -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 {
+87
View File
@@ -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 (
@@ -118,7 +118,10 @@ export const WorkspaceSchedulePage: FC = () => {
await submitScheduleMutation.mutateAsync(data);
if (data.autostopChanged) {
if (
data.autostopChanged &&
getAutostop(workspace).autostopEnabled
) {
setIsConfirmingApply(true);
}
}}