-
Notifications
You must be signed in to change notification settings - Fork 894
feat: allow bumping workspace deadline #1828
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 8 commits
39a53c2
f764e61
2a74312
6f2bef5
dcd57cf
c31b9dd
23247d4
41306aa
d9a795f
3ecb323
4b868d2
cfeb274
b5be82c
9a45186
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
package cli | ||
|
||
import ( | ||
"fmt" | ||
"strings" | ||
"time" | ||
|
||
"github.com/spf13/cobra" | ||
"golang.org/x/xerrors" | ||
|
||
"github.com/coder/coder/codersdk" | ||
) | ||
|
||
const ( | ||
bumpDescriptionLong = `To extend the autostop deadline for a workspace. | ||
If no unit is specified in the duration, we assume minutes. | ||
` | ||
johnstcn marked this conversation as resolved.
Show resolved
Hide resolved
|
||
defaultBumpDuration = 90 * time.Minute | ||
) | ||
|
||
func bump() *cobra.Command { | ||
bumpCmd := &cobra.Command{ | ||
Args: cobra.RangeArgs(1, 2), | ||
Annotations: workspaceCommand, | ||
Use: "bump workspace [duration]", | ||
johnstcn marked this conversation as resolved.
Show resolved
Hide resolved
johnstcn marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Short: "Extend the autostop deadline for a workspace.", | ||
Long: bumpDescriptionLong, | ||
Example: "coder bump my-workspace 90m", | ||
RunE: func(cmd *cobra.Command, args []string) error { | ||
bumpDuration := defaultBumpDuration | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we have a default, should it just be a flag? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't like the way that looks, without the flag is less typing! |
||
if len(args) > 1 { | ||
d, err := tryParseDuration(args[1]) | ||
if err != nil { | ||
return err | ||
} | ||
bumpDuration = d | ||
} | ||
|
||
if bumpDuration < time.Minute { | ||
return xerrors.New("minimum bump duration is 1 minute") | ||
} | ||
|
||
client, err := createClient(cmd) | ||
if err != nil { | ||
return xerrors.Errorf("create client: %w", err) | ||
} | ||
organization, err := currentOrganization(cmd, client) | ||
if err != nil { | ||
return xerrors.Errorf("get current org: %w", err) | ||
} | ||
|
||
workspace, err := client.WorkspaceByOwnerAndName(cmd.Context(), organization.ID, codersdk.Me, args[0]) | ||
if err != nil { | ||
return xerrors.Errorf("get workspace: %w", err) | ||
} | ||
|
||
if workspace.LatestBuild.Deadline.IsZero() { | ||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "no deadline set\n") | ||
return nil | ||
} | ||
Emyrk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
newDeadline := workspace.LatestBuild.Deadline.Add(bumpDuration) | ||
if err := client.PutExtendWorkspace(cmd.Context(), workspace.ID, codersdk.PutExtendWorkspaceRequest{ | ||
Deadline: newDeadline, | ||
}); err != nil { | ||
return err | ||
} | ||
|
||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Workspace %q will now stop at %s\n", workspace.Name, newDeadline.Format(time.RFC3339)) | ||
|
||
return nil | ||
}, | ||
} | ||
|
||
return bumpCmd | ||
} | ||
|
||
func tryParseDuration(raw string) (time.Duration, error) { | ||
// If the user input a raw number, assume minutes | ||
if isDigit(raw) { | ||
raw = raw + "m" | ||
} | ||
d, err := time.ParseDuration(raw) | ||
if err != nil { | ||
return 0, err | ||
} | ||
return d, nil | ||
} | ||
Emyrk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
func isDigit(s string) bool { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This would also return true for the empty string, |
||
return strings.IndexFunc(s, func(c rune) bool { | ||
return c < '0' || c > '9' | ||
}) == -1 | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,218 @@ | ||
package cli_test | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"testing" | ||
"time" | ||
|
||
"github.com/stretchr/testify/require" | ||
|
||
"github.com/coder/coder/cli/clitest" | ||
"github.com/coder/coder/coderd/coderdtest" | ||
"github.com/coder/coder/codersdk" | ||
) | ||
|
||
func TestBump(t *testing.T) { | ||
t.Parallel() | ||
|
||
t.Run("BumpOKDefault", func(t *testing.T) { | ||
t.Parallel() | ||
|
||
// Given: we have a workspace | ||
var ( | ||
err error | ||
ctx = context.Background() | ||
client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) | ||
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, user.OrganizationID, project.ID) | ||
cmdArgs = []string{"bump", workspace.Name} | ||
stdoutBuf = &bytes.Buffer{} | ||
) | ||
|
||
// Given: we wait for the workspace to be built | ||
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) | ||
workspace, err = client.Workspace(ctx, workspace.ID) | ||
require.NoError(t, err) | ||
expectedDeadline := workspace.LatestBuild.Deadline.Add(90 * time.Minute) | ||
|
||
// Assert test invariant: workspace build has a deadline set equal to now plus ttl | ||
require.WithinDuration(t, workspace.LatestBuild.Deadline, time.Now().Add(*workspace.TTL), time.Minute) | ||
require.NoError(t, err) | ||
|
||
cmd, root := clitest.New(t, cmdArgs...) | ||
clitest.SetupConfig(t, client, root) | ||
cmd.SetOut(stdoutBuf) | ||
|
||
// When: we execute `coder bump <workspace>` | ||
err = cmd.ExecuteContext(ctx) | ||
require.NoError(t, err, "unexpected error") | ||
|
||
// Then: the deadline of the latest build is updated | ||
updated, err := client.Workspace(ctx, workspace.ID) | ||
require.NoError(t, err) | ||
require.WithinDuration(t, expectedDeadline, updated.LatestBuild.Deadline, time.Minute) | ||
}) | ||
|
||
t.Run("BumpSpecificDuration", func(t *testing.T) { | ||
t.Parallel() | ||
|
||
// Given: we have a workspace | ||
var ( | ||
err error | ||
ctx = context.Background() | ||
client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) | ||
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, user.OrganizationID, project.ID) | ||
cmdArgs = []string{"bump", workspace.Name, "30"} | ||
stdoutBuf = &bytes.Buffer{} | ||
) | ||
|
||
// Given: we wait for the workspace to be built | ||
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) | ||
workspace, err = client.Workspace(ctx, workspace.ID) | ||
require.NoError(t, err) | ||
expectedDeadline := workspace.LatestBuild.Deadline.Add(30 * time.Minute) | ||
|
||
// Assert test invariant: workspace build has a deadline set equal to now plus ttl | ||
require.WithinDuration(t, workspace.LatestBuild.Deadline, time.Now().Add(*workspace.TTL), time.Minute) | ||
require.NoError(t, err) | ||
|
||
cmd, root := clitest.New(t, cmdArgs...) | ||
clitest.SetupConfig(t, client, root) | ||
cmd.SetOut(stdoutBuf) | ||
|
||
// When: we execute `coder bump workspace <number without units>` | ||
err = cmd.ExecuteContext(ctx) | ||
require.NoError(t, err) | ||
|
||
// Then: the deadline of the latest build is updated assuming the units are minutes | ||
updated, err := client.Workspace(ctx, workspace.ID) | ||
require.NoError(t, err) | ||
require.WithinDuration(t, expectedDeadline, updated.LatestBuild.Deadline, time.Minute) | ||
}) | ||
|
||
t.Run("BumpInvalidDuration", func(t *testing.T) { | ||
t.Parallel() | ||
|
||
// Given: we have a workspace | ||
var ( | ||
err error | ||
ctx = context.Background() | ||
client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) | ||
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, user.OrganizationID, project.ID) | ||
cmdArgs = []string{"bump", workspace.Name, "kwyjibo"} | ||
johnstcn marked this conversation as resolved.
Show resolved
Hide resolved
|
||
stdoutBuf = &bytes.Buffer{} | ||
) | ||
|
||
// Given: we wait for the workspace to be built | ||
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) | ||
workspace, err = client.Workspace(ctx, workspace.ID) | ||
require.NoError(t, err) | ||
|
||
// Assert test invariant: workspace build has a deadline set equal to now plus ttl | ||
require.WithinDuration(t, workspace.LatestBuild.Deadline, time.Now().Add(*workspace.TTL), time.Minute) | ||
require.NoError(t, err) | ||
|
||
cmd, root := clitest.New(t, cmdArgs...) | ||
clitest.SetupConfig(t, client, root) | ||
cmd.SetOut(stdoutBuf) | ||
|
||
// When: we execute `coder bump workspace <not a number>` | ||
err = cmd.ExecuteContext(ctx) | ||
// Then: the command fails | ||
require.ErrorContains(t, err, "invalid duration") | ||
}) | ||
|
||
t.Run("BumpNoDeadline", func(t *testing.T) { | ||
t.Parallel() | ||
|
||
// Given: we have a workspace with no deadline set | ||
var ( | ||
err error | ||
ctx = context.Background() | ||
client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) | ||
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, user.OrganizationID, project.ID, func(cwr *codersdk.CreateWorkspaceRequest) { | ||
cwr.TTL = nil | ||
}) | ||
cmdArgs = []string{"bump", workspace.Name} | ||
stdoutBuf = &bytes.Buffer{} | ||
) | ||
|
||
// Given: we wait for the workspace to build | ||
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) | ||
workspace, err = client.Workspace(ctx, workspace.ID) | ||
require.NoError(t, err) | ||
|
||
// Assert test invariant: workspace has no TTL set | ||
require.Zero(t, workspace.LatestBuild.Deadline) | ||
require.NoError(t, err) | ||
|
||
cmd, root := clitest.New(t, cmdArgs...) | ||
clitest.SetupConfig(t, client, root) | ||
cmd.SetOut(stdoutBuf) | ||
|
||
// When: we execute `coder bump workspace`` | ||
err = cmd.ExecuteContext(ctx) | ||
|
||
// Then: nothing happens and the deadline remains unset | ||
require.NoError(t, err) | ||
updated, err := client.Workspace(ctx, workspace.ID) | ||
johnstcn marked this conversation as resolved.
Show resolved
Hide resolved
|
||
require.NoError(t, err) | ||
require.Zero(t, updated.LatestBuild.Deadline) | ||
}) | ||
|
||
t.Run("BumpMinimumDuration", func(t *testing.T) { | ||
t.Parallel() | ||
|
||
// Given: we have a workspace with no deadline set | ||
var ( | ||
err error | ||
ctx = context.Background() | ||
client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) | ||
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, user.OrganizationID, project.ID) | ||
cmdArgs = []string{"bump", workspace.Name, "59s"} | ||
stdoutBuf = &bytes.Buffer{} | ||
) | ||
|
||
// Given: we wait for the workspace to build | ||
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) | ||
workspace, err = client.Workspace(ctx, workspace.ID) | ||
require.NoError(t, err) | ||
|
||
// Assert test invariant: workspace build has a deadline set equal to now plus ttl | ||
require.WithinDuration(t, workspace.LatestBuild.Deadline, time.Now().Add(*workspace.TTL), time.Minute) | ||
require.NoError(t, err) | ||
|
||
cmd, root := clitest.New(t, cmdArgs...) | ||
clitest.SetupConfig(t, client, root) | ||
cmd.SetOut(stdoutBuf) | ||
|
||
// When: we execute `coder bump workspace 59s` | ||
err = cmd.ExecuteContext(ctx) | ||
|
||
// Then: an error is reported and the deadline remains as before | ||
require.ErrorContains(t, err, "minimum bump duration is 1 minute") | ||
johnstcn marked this conversation as resolved.
Show resolved
Hide resolved
|
||
updated, err := client.Workspace(ctx, workspace.ID) | ||
require.NoError(t, err) | ||
require.WithinDuration(t, workspace.LatestBuild.Deadline, updated.LatestBuild.Deadline, time.Minute) | ||
}) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -67,6 +67,7 @@ func Root() *cobra.Command { | |
|
||
cmd.AddCommand( | ||
autostart(), | ||
bump(), | ||
configSSH(), | ||
create(), | ||
delete(), | ||
|
Uh oh!
There was an error while loading. Please reload this page.