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

Skip to content

Commit 119db78

Browse files
authored
feat: update workspace deadline when workspace ttl updated (#2165)
This commit adds the following changes to workspace scheduling behaviour: * CLI: updating a workspace TTL updates the deadline of the workspace. * If the TTL is being un-set, the workspace deadline is set to zero. * If the TTL is being set, the workspace deadline is updated to be the last updated time of the workspace build plus the requested TTL. Additionally, the user is prompted to confirm interactively (can be bypassed with -y). * UI: updating the workspace schedule behaves similarly to the CLI, showing a message to the user if the updated TTL/time to shutdown would effect changes to the lifetime of the running workspace.
1 parent 411d7da commit 119db78

File tree

9 files changed

+358
-54
lines changed

9 files changed

+358
-54
lines changed

cli/ttl.go

+41
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
package cli
22

33
import (
4+
"errors"
45
"fmt"
56
"time"
67

78
"github.com/spf13/cobra"
89
"golang.org/x/xerrors"
910

11+
"github.com/coder/coder/cli/cliui"
1012
"github.com/coder/coder/codersdk"
1113
)
1214

@@ -89,6 +91,30 @@ func ttlset() *cobra.Command {
8991
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "warning: ttl rounded down to %s\n", truncated)
9092
}
9193

94+
if changed, newDeadline := changedNewDeadline(workspace, truncated); changed {
95+
// For the purposes of the user, "less than a minute" is essentially the same as "immediately".
96+
timeRemaining := time.Until(newDeadline).Truncate(time.Minute)
97+
humanRemaining := "in " + timeRemaining.String()
98+
if timeRemaining <= 0 {
99+
humanRemaining = "immediately"
100+
}
101+
_, err = cliui.Prompt(cmd, cliui.PromptOptions{
102+
Text: fmt.Sprintf(
103+
"Workspace %q will be stopped %s. Are you sure?",
104+
workspace.Name,
105+
humanRemaining,
106+
),
107+
Default: "yes",
108+
IsConfirm: true,
109+
})
110+
if err != nil {
111+
if errors.Is(err, cliui.Canceled) {
112+
return nil
113+
}
114+
return err
115+
}
116+
}
117+
92118
millis := truncated.Milliseconds()
93119
if err = client.UpdateWorkspaceTTL(cmd.Context(), workspace.ID, codersdk.UpdateWorkspaceTTLRequest{
94120
TTLMillis: &millis,
@@ -131,3 +157,18 @@ func ttlunset() *cobra.Command {
131157
},
132158
}
133159
}
160+
161+
func changedNewDeadline(ws codersdk.Workspace, newTTL time.Duration) (changed bool, newDeadline time.Time) {
162+
if ws.LatestBuild.Transition != codersdk.WorkspaceTransitionStart {
163+
// not running
164+
return false, newDeadline
165+
}
166+
167+
if ws.LatestBuild.Job.CompletedAt == nil {
168+
// still building
169+
return false, newDeadline
170+
}
171+
172+
newDeadline = ws.LatestBuild.Job.CompletedAt.Add(newTTL)
173+
return true, newDeadline
174+
}

cli/ttl_test.go

+53-24
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,19 @@ package cli_test
33
import (
44
"bytes"
55
"context"
6+
"fmt"
67
"strings"
78
"testing"
89
"time"
910

11+
"github.com/stretchr/testify/assert"
1012
"github.com/stretchr/testify/require"
1113

1214
"github.com/coder/coder/cli/clitest"
1315
"github.com/coder/coder/coderd/coderdtest"
1416
"github.com/coder/coder/coderd/util/ptr"
1517
"github.com/coder/coder/codersdk"
18+
"github.com/coder/coder/pty/ptytest"
1619
)
1720

1821
func TestTTL(t *testing.T) {
@@ -22,33 +25,29 @@ func TestTTL(t *testing.T) {
2225
t.Parallel()
2326

2427
var (
25-
ctx = context.Background()
2628
client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
2729
user = coderdtest.CreateFirstUser(t, client)
2830
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
2931
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
30-
project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
31-
workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID)
32+
template = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
33+
ttl = 7*time.Hour + 30*time.Minute + 30*time.Second
34+
workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
35+
cwr.TTLMillis = ptr.Ref(ttl.Milliseconds())
36+
})
3237
cmdArgs = []string{"ttl", "show", workspace.Name}
33-
ttl = 8*time.Hour + 30*time.Minute + 30*time.Second
3438
stdoutBuf = &bytes.Buffer{}
3539
)
3640

37-
err := client.UpdateWorkspaceTTL(ctx, workspace.ID, codersdk.UpdateWorkspaceTTLRequest{
38-
TTLMillis: ptr.Ref(ttl.Milliseconds()),
39-
})
40-
require.NoError(t, err)
41-
4241
cmd, root := clitest.New(t, cmdArgs...)
4342
clitest.SetupConfig(t, client, root)
4443
cmd.SetOut(stdoutBuf)
4544

46-
err = cmd.Execute()
45+
err := cmd.Execute()
4746
require.NoError(t, err, "unexpected error")
4847
require.Equal(t, ttl.Truncate(time.Minute).String(), strings.TrimSpace(stdoutBuf.String()))
4948
})
5049

51-
t.Run("SetUnsetOK", func(t *testing.T) {
50+
t.Run("UnsetOK", func(t *testing.T) {
5251
t.Parallel()
5352

5453
var (
@@ -58,9 +57,11 @@ func TestTTL(t *testing.T) {
5857
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
5958
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
6059
project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
61-
workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID)
6260
ttl = 8*time.Hour + 30*time.Minute + 30*time.Second
63-
cmdArgs = []string{"ttl", "set", workspace.Name, ttl.String()}
61+
workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
62+
cwr.TTLMillis = ptr.Ref(ttl.Milliseconds())
63+
})
64+
cmdArgs = []string{"ttl", "unset", workspace.Name}
6465
stdoutBuf = &bytes.Buffer{}
6566
)
6667

@@ -71,24 +72,52 @@ func TestTTL(t *testing.T) {
7172
err := cmd.Execute()
7273
require.NoError(t, err, "unexpected error")
7374

74-
// Ensure ttl updated
75+
// Ensure ttl unset
7576
updated, err := client.Workspace(ctx, workspace.ID)
7677
require.NoError(t, err, "fetch updated workspace")
77-
require.Equal(t, ttl.Truncate(time.Minute), time.Duration(*updated.TTLMillis)*time.Millisecond)
78-
require.Contains(t, stdoutBuf.String(), "warning: ttl rounded down")
78+
require.Nil(t, updated.TTLMillis, "expected ttl to not be set")
79+
})
7980

80-
// unset schedule
81-
cmd, root = clitest.New(t, "ttl", "unset", workspace.Name)
82-
clitest.SetupConfig(t, client, root)
83-
cmd.SetOut(stdoutBuf)
81+
t.Run("SetOK", func(t *testing.T) {
82+
t.Parallel()
8483

85-
err = cmd.Execute()
86-
require.NoError(t, err, "unexpected error")
84+
var (
85+
ctx = context.Background()
86+
client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
87+
user = coderdtest.CreateFirstUser(t, client)
88+
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
89+
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
90+
project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
91+
ttl = 8*time.Hour + 30*time.Minute + 30*time.Second
92+
workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
93+
cwr.TTLMillis = ptr.Ref(ttl.Milliseconds())
94+
})
95+
_ = coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
96+
cmdArgs = []string{"ttl", "set", workspace.Name, ttl.String()}
97+
done = make(chan struct{})
98+
)
8799

100+
cmd, root := clitest.New(t, cmdArgs...)
101+
clitest.SetupConfig(t, client, root)
102+
pty := ptytest.New(t)
103+
cmd.SetIn(pty.Input())
104+
cmd.SetOut(pty.Output())
105+
106+
go func() {
107+
defer close(done)
108+
err := cmd.Execute()
109+
assert.NoError(t, err, "unexpected error")
110+
}()
111+
112+
pty.ExpectMatch(fmt.Sprintf("warning: ttl rounded down to %s", ttl.Truncate(time.Minute)))
113+
pty.ExpectMatch(fmt.Sprintf("Workspace %q will be stopped in 8h29m0s. Are you sure?", workspace.Name))
114+
pty.WriteLine("yes")
88115
// Ensure ttl updated
89-
updated, err = client.Workspace(ctx, workspace.ID)
116+
updated, err := client.Workspace(ctx, workspace.ID)
90117
require.NoError(t, err, "fetch updated workspace")
91-
require.Nil(t, updated.TTLMillis, "expected ttl to not be set")
118+
require.Equal(t, ttl.Truncate(time.Minute), time.Duration(*updated.TTLMillis)*time.Millisecond)
119+
120+
<-done
92121
})
93122

94123
t.Run("ZeroInvalid", func(t *testing.T) {

coderd/autobuild/executor/lifecycle_executor_test.go

+28-5
Original file line numberDiff line numberDiff line change
@@ -440,18 +440,41 @@ func TestExecutorWorkspaceAutostopNoWaitChangedMyMind(t *testing.T) {
440440
err := client.UpdateWorkspaceTTL(ctx, workspace.ID, codersdk.UpdateWorkspaceTTLRequest{TTLMillis: nil})
441441
require.NoError(t, err)
442442

443-
// When: the autobuild executor ticks after the deadline
443+
// Then: the deadline should be the zero value
444+
updated := coderdtest.MustWorkspace(t, client, workspace.ID)
445+
assert.Zero(t, updated.LatestBuild.Deadline)
446+
447+
// When: the autobuild executor ticks after the original deadline
444448
go func() {
445449
tickCh <- workspace.LatestBuild.Deadline.Add(time.Minute)
446-
close(tickCh)
447450
}()
448451

449-
// Then: the workspace should still stop - sorry!
452+
// Then: the workspace should not stop
450453
stats := <-statsCh
451454
assert.NoError(t, stats.Error)
455+
assert.Len(t, stats.Transitions, 0)
456+
457+
// Given: the user changes their mind again and wants to enable auto-stop
458+
newTTL := 8 * time.Hour
459+
expectedDeadline := workspace.LatestBuild.UpdatedAt.Add(newTTL)
460+
err = client.UpdateWorkspaceTTL(ctx, workspace.ID, codersdk.UpdateWorkspaceTTLRequest{TTLMillis: ptr.Ref(newTTL.Milliseconds())})
461+
require.NoError(t, err)
462+
463+
// Then: the deadline should be updated based on the TTL
464+
updated = coderdtest.MustWorkspace(t, client, workspace.ID)
465+
assert.WithinDuration(t, expectedDeadline, updated.LatestBuild.Deadline, time.Minute)
466+
467+
// When: the relentless onward march of time continues
468+
go func() {
469+
tickCh <- workspace.LatestBuild.Deadline.Add(newTTL + time.Minute)
470+
close(tickCh)
471+
}()
472+
473+
// Then: the workspace should stop
474+
stats = <-statsCh
475+
assert.NoError(t, stats.Error)
452476
assert.Len(t, stats.Transitions, 1)
453-
assert.Contains(t, stats.Transitions, workspace.ID)
454-
assert.Equal(t, database.WorkspaceTransitionStop, stats.Transitions[workspace.ID])
477+
assert.Equal(t, stats.Transitions[workspace.ID], database.WorkspaceTransitionStop)
455478
}
456479

457480
func TestExecutorAutostartMultipleOK(t *testing.T) {

coderd/workspaces.go

+44-4
Original file line numberDiff line numberDiff line change
@@ -566,17 +566,57 @@ func (api *API) putWorkspaceTTL(rw http.ResponseWriter, r *http.Request) {
566566
return
567567
}
568568

569-
err = api.Database.UpdateWorkspaceTTL(r.Context(), database.UpdateWorkspaceTTLParams{
570-
ID: workspace.ID,
571-
Ttl: dbTTL,
569+
err = api.Database.InTx(func(s database.Store) error {
570+
if err := s.UpdateWorkspaceTTL(r.Context(), database.UpdateWorkspaceTTLParams{
571+
ID: workspace.ID,
572+
Ttl: dbTTL,
573+
}); err != nil {
574+
return xerrors.Errorf("update workspace TTL: %w", err)
575+
}
576+
577+
// Also extend the workspace deadline if the workspace is running
578+
latestBuild, err := s.GetLatestWorkspaceBuildByWorkspaceID(r.Context(), workspace.ID)
579+
if err != nil {
580+
return xerrors.Errorf("get latest workspace build: %w", err)
581+
}
582+
583+
if latestBuild.Transition != database.WorkspaceTransitionStart {
584+
return nil // nothing to do
585+
}
586+
587+
if latestBuild.UpdatedAt.IsZero() {
588+
// Build in progress; provisionerd should update with the new TTL.
589+
return nil
590+
}
591+
592+
var newDeadline time.Time
593+
if dbTTL.Valid {
594+
newDeadline = latestBuild.UpdatedAt.Add(time.Duration(dbTTL.Int64))
595+
}
596+
597+
if err := s.UpdateWorkspaceBuildByID(
598+
r.Context(),
599+
database.UpdateWorkspaceBuildByIDParams{
600+
ID: latestBuild.ID,
601+
UpdatedAt: latestBuild.UpdatedAt,
602+
ProvisionerState: latestBuild.ProvisionerState,
603+
Deadline: newDeadline,
604+
},
605+
); err != nil {
606+
return xerrors.Errorf("update workspace deadline: %w", err)
607+
}
608+
return nil
572609
})
610+
573611
if err != nil {
574612
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
575-
Message: "Internal error updating workspace TTL.",
613+
Message: "Error updating workspace time until shutdown!",
576614
Detail: err.Error(),
577615
})
578616
return
579617
}
618+
619+
httpapi.Write(rw, http.StatusOK, nil)
580620
}
581621

582622
func (api *API) putExtendWorkspace(rw http.ResponseWriter, r *http.Request) {

0 commit comments

Comments
 (0)