mirror of
https://github.com/coder/coder.git
synced 2025-07-13 21:36:50 +00:00
feat: allow bumping workspace deadline (#1828)
* Adds a `bump` command to extend workspace build deadline * Reduces WARN-level logging spam from autobuild executor * Modifies `cli/ssh` notifications to read from workspace build deadline and to notify relative time instead (sidestepping the problem of figuring out a user's timezone across multiple OSes) * Shows workspace extension time in `coder list` output e.g. ``` WORKSPACE TEMPLATE STATUS LAST BUILT OUTDATED AUTOSTART TTL developer/test1 docker Running 4m false 0 9 * * MON-FRI 15m (+5m) ```
This commit is contained in:
93
cli/bump.go
Normal file
93
cli/bump.go
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
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.`
|
||||||
|
defaultBumpDuration = 90 * time.Minute
|
||||||
|
)
|
||||||
|
|
||||||
|
func bump() *cobra.Command {
|
||||||
|
bumpCmd := &cobra.Command{
|
||||||
|
Args: cobra.RangeArgs(1, 2),
|
||||||
|
Annotations: workspaceCommand,
|
||||||
|
Use: "bump <workspace-name> [duration]",
|
||||||
|
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
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func isDigit(s string) bool {
|
||||||
|
return strings.IndexFunc(s, func(c rune) bool {
|
||||||
|
return c < '0' || c > '9'
|
||||||
|
}) == -1
|
||||||
|
}
|
218
cli/bump_test.go
Normal file
218
cli/bump_test.go
Normal file
@ -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"}
|
||||||
|
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)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Then: nothing happens and the deadline remains unset
|
||||||
|
updated, err := client.Workspace(ctx, workspace.ID)
|
||||||
|
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)
|
||||||
|
require.ErrorContains(t, err, "minimum bump duration is 1 minute")
|
||||||
|
|
||||||
|
// Then: an error is reported and the deadline remains as before
|
||||||
|
updated, err := client.Workspace(ctx, workspace.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.WithinDuration(t, workspace.LatestBuild.Deadline, updated.LatestBuild.Deadline, time.Minute)
|
||||||
|
})
|
||||||
|
}
|
73
cli/list.go
73
cli/list.go
@ -86,28 +86,6 @@ func list() *cobra.Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
duration := time.Now().UTC().Sub(workspace.LatestBuild.Job.CreatedAt).Truncate(time.Second)
|
duration := time.Now().UTC().Sub(workspace.LatestBuild.Job.CreatedAt).Truncate(time.Second)
|
||||||
if duration > time.Hour {
|
|
||||||
duration = duration.Truncate(time.Hour)
|
|
||||||
}
|
|
||||||
if duration > time.Minute {
|
|
||||||
duration = duration.Truncate(time.Minute)
|
|
||||||
}
|
|
||||||
days := 0
|
|
||||||
for duration.Hours() > 24 {
|
|
||||||
days++
|
|
||||||
duration -= 24 * time.Hour
|
|
||||||
}
|
|
||||||
durationDisplay := duration.String()
|
|
||||||
if days > 0 {
|
|
||||||
durationDisplay = fmt.Sprintf("%dd%s", days, durationDisplay)
|
|
||||||
}
|
|
||||||
if strings.HasSuffix(durationDisplay, "m0s") {
|
|
||||||
durationDisplay = durationDisplay[:len(durationDisplay)-2]
|
|
||||||
}
|
|
||||||
if strings.HasSuffix(durationDisplay, "h0m") {
|
|
||||||
durationDisplay = durationDisplay[:len(durationDisplay)-2]
|
|
||||||
}
|
|
||||||
|
|
||||||
autostartDisplay := "-"
|
autostartDisplay := "-"
|
||||||
if workspace.AutostartSchedule != "" {
|
if workspace.AutostartSchedule != "" {
|
||||||
if sched, err := schedule.Weekly(workspace.AutostartSchedule); err == nil {
|
if sched, err := schedule.Weekly(workspace.AutostartSchedule); err == nil {
|
||||||
@ -117,7 +95,10 @@ func list() *cobra.Command {
|
|||||||
|
|
||||||
autostopDisplay := "-"
|
autostopDisplay := "-"
|
||||||
if workspace.TTL != nil {
|
if workspace.TTL != nil {
|
||||||
autostopDisplay = workspace.TTL.String()
|
autostopDisplay = durationDisplay(*workspace.TTL)
|
||||||
|
if has, ext := hasExtension(workspace); has {
|
||||||
|
autostopDisplay += fmt.Sprintf(" (+%s)", durationDisplay(ext.Round(time.Minute)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
user := usersByID[workspace.OwnerID]
|
user := usersByID[workspace.OwnerID]
|
||||||
@ -125,7 +106,7 @@ func list() *cobra.Command {
|
|||||||
user.Username + "/" + workspace.Name,
|
user.Username + "/" + workspace.Name,
|
||||||
workspace.TemplateName,
|
workspace.TemplateName,
|
||||||
status,
|
status,
|
||||||
durationDisplay,
|
durationDisplay(duration),
|
||||||
workspace.Outdated,
|
workspace.Outdated,
|
||||||
autostartDisplay,
|
autostartDisplay,
|
||||||
autostopDisplay,
|
autostopDisplay,
|
||||||
@ -139,3 +120,47 @@ func list() *cobra.Command {
|
|||||||
"Specify a column to filter in the table.")
|
"Specify a column to filter in the table.")
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func hasExtension(ws codersdk.Workspace) (bool, time.Duration) {
|
||||||
|
if ws.LatestBuild.Transition != codersdk.WorkspaceTransitionStart {
|
||||||
|
return false, 0
|
||||||
|
}
|
||||||
|
if ws.LatestBuild.Deadline.IsZero() {
|
||||||
|
return false, 0
|
||||||
|
}
|
||||||
|
if ws.TTL == nil {
|
||||||
|
return false, 0
|
||||||
|
}
|
||||||
|
delta := ws.LatestBuild.Deadline.Add(-*ws.TTL).Sub(ws.LatestBuild.CreatedAt)
|
||||||
|
if delta < time.Minute {
|
||||||
|
return false, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, delta
|
||||||
|
}
|
||||||
|
|
||||||
|
func durationDisplay(d time.Duration) string {
|
||||||
|
duration := d
|
||||||
|
if duration > time.Hour {
|
||||||
|
duration = duration.Truncate(time.Hour)
|
||||||
|
}
|
||||||
|
if duration > time.Minute {
|
||||||
|
duration = duration.Truncate(time.Minute)
|
||||||
|
}
|
||||||
|
days := 0
|
||||||
|
for duration.Hours() > 24 {
|
||||||
|
days++
|
||||||
|
duration -= 24 * time.Hour
|
||||||
|
}
|
||||||
|
durationDisplay := duration.String()
|
||||||
|
if days > 0 {
|
||||||
|
durationDisplay = fmt.Sprintf("%dd%s", days, durationDisplay)
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(durationDisplay, "m0s") {
|
||||||
|
durationDisplay = durationDisplay[:len(durationDisplay)-2]
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(durationDisplay, "h0m") {
|
||||||
|
durationDisplay = durationDisplay[:len(durationDisplay)-2]
|
||||||
|
}
|
||||||
|
return durationDisplay
|
||||||
|
}
|
||||||
|
@ -67,6 +67,7 @@ func Root() *cobra.Command {
|
|||||||
|
|
||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
autostart(),
|
autostart(),
|
||||||
|
bump(),
|
||||||
configSSH(),
|
configSSH(),
|
||||||
create(),
|
create(),
|
||||||
delete(),
|
delete(),
|
||||||
|
@ -289,17 +289,14 @@ func notifyCondition(ctx context.Context, client *codersdk.Client, workspaceID u
|
|||||||
return time.Time{}, nil
|
return time.Time{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
deadline = ws.LatestBuild.UpdatedAt.Add(*ws.TTL)
|
deadline = ws.LatestBuild.Deadline
|
||||||
callback = func() {
|
callback = func() {
|
||||||
ttl := deadline.Sub(now)
|
ttl := deadline.Sub(now)
|
||||||
var title, body string
|
var title, body string
|
||||||
if ttl > time.Minute {
|
if ttl > time.Minute {
|
||||||
title = fmt.Sprintf(`Workspace %s stopping in %.0f mins`, ws.Name, ttl.Minutes())
|
title = fmt.Sprintf(`Workspace %s stopping soon`, ws.Name)
|
||||||
body = fmt.Sprintf(
|
body = fmt.Sprintf(
|
||||||
`Your Coder workspace %s is scheduled to stop at %s.`,
|
`Your Coder workspace %s is scheduled to stop in %.0f mins`, ws.Name, ttl.Minutes())
|
||||||
ws.Name,
|
|
||||||
deadline.Format(time.Kitchen),
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
title = fmt.Sprintf("Workspace %s stopping!", ws.Name)
|
title = fmt.Sprintf("Workspace %s stopping!", ws.Name)
|
||||||
body = fmt.Sprintf("Your Coder workspace %s is stopping any time now!", ws.Name)
|
body = fmt.Sprintf("Your Coder workspace %s is stopping any time now!", ws.Name)
|
||||||
|
@ -88,7 +88,7 @@ func (e *Executor) runOnce(t time.Time) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !priorJob.CompletedAt.Valid || priorJob.Error.String != "" {
|
if !priorJob.CompletedAt.Valid || priorJob.Error.String != "" {
|
||||||
e.log.Warn(e.ctx, "last workspace build did not complete successfully, skipping",
|
e.log.Debug(e.ctx, "last workspace build did not complete successfully, skipping",
|
||||||
slog.F("workspace_id", ws.ID),
|
slog.F("workspace_id", ws.ID),
|
||||||
slog.F("error", priorJob.Error.String),
|
slog.F("error", priorJob.Error.String),
|
||||||
)
|
)
|
||||||
@ -107,13 +107,14 @@ func (e *Executor) runOnce(t time.Time) error {
|
|||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Truncate to nearest minute for consistency with autostart behavior
|
// For stopping, do not truncate. This is inconsistent with autostart, but
|
||||||
nextTransition = priorHistory.Deadline.Truncate(time.Minute)
|
// it ensures we will not stop too early.
|
||||||
|
nextTransition = priorHistory.Deadline
|
||||||
case database.WorkspaceTransitionStop:
|
case database.WorkspaceTransitionStop:
|
||||||
validTransition = database.WorkspaceTransitionStart
|
validTransition = database.WorkspaceTransitionStart
|
||||||
sched, err := schedule.Weekly(ws.AutostartSchedule.String)
|
sched, err := schedule.Weekly(ws.AutostartSchedule.String)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
e.log.Warn(e.ctx, "workspace has invalid autostart schedule, skipping",
|
e.log.Debug(e.ctx, "workspace has invalid autostart schedule, skipping",
|
||||||
slog.F("workspace_id", ws.ID),
|
slog.F("workspace_id", ws.ID),
|
||||||
slog.F("autostart_schedule", ws.AutostartSchedule.String),
|
slog.F("autostart_schedule", ws.AutostartSchedule.String),
|
||||||
)
|
)
|
||||||
|
@ -550,7 +550,7 @@ func (server *provisionerdServer) CompleteJob(ctx context.Context, completed *pr
|
|||||||
workspace, err := db.GetWorkspaceByID(ctx, workspaceBuild.WorkspaceID)
|
workspace, err := db.GetWorkspaceByID(ctx, workspaceBuild.WorkspaceID)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if workspace.Ttl.Valid {
|
if workspace.Ttl.Valid {
|
||||||
workspaceDeadline = now.Add(time.Duration(workspace.Ttl.Int64)).Truncate(time.Minute)
|
workspaceDeadline = now.Add(time.Duration(workspace.Ttl.Int64))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Huh? Did the workspace get deleted?
|
// Huh? Did the workspace get deleted?
|
||||||
|
@ -604,7 +604,7 @@ func (api *API) putExtendWorkspace(rw http.ResponseWriter, r *http.Request) {
|
|||||||
return xerrors.Errorf("workspace must be started, current status: %s", build.Transition)
|
return xerrors.Errorf("workspace must be started, current status: %s", build.Transition)
|
||||||
}
|
}
|
||||||
|
|
||||||
newDeadline := req.Deadline.Truncate(time.Minute).UTC()
|
newDeadline := req.Deadline.UTC()
|
||||||
if newDeadline.IsZero() {
|
if newDeadline.IsZero() {
|
||||||
// This should not be possible because the struct validation field enforces a non-zero value.
|
// This should not be possible because the struct validation field enforces a non-zero value.
|
||||||
code = http.StatusBadRequest
|
code = http.StatusBadRequest
|
||||||
@ -616,8 +616,8 @@ func (api *API) putExtendWorkspace(rw http.ResponseWriter, r *http.Request) {
|
|||||||
return xerrors.Errorf("new deadline %q must be after existing deadline %q", newDeadline.Format(time.RFC3339), build.Deadline.Format(time.RFC3339))
|
return xerrors.Errorf("new deadline %q must be after existing deadline %q", newDeadline.Format(time.RFC3339), build.Deadline.Format(time.RFC3339))
|
||||||
}
|
}
|
||||||
|
|
||||||
// both newDeadline and build.Deadline are truncated to time.Minute
|
// Disallow updates within less than one minute
|
||||||
if newDeadline == build.Deadline {
|
if withinDuration(newDeadline, build.Deadline, time.Minute) {
|
||||||
code = http.StatusNotModified
|
code = http.StatusNotModified
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -786,6 +786,7 @@ func convertWorkspaces(ctx context.Context, db database.Store, workspaces []data
|
|||||||
InitiatorID: workspaceBuild.InitiatorID,
|
InitiatorID: workspaceBuild.InitiatorID,
|
||||||
ProvisionerState: workspaceBuild.ProvisionerState,
|
ProvisionerState: workspaceBuild.ProvisionerState,
|
||||||
JobID: workspaceBuild.JobID,
|
JobID: workspaceBuild.JobID,
|
||||||
|
Deadline: workspaceBuild.Deadline,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
templateByID := map[uuid.UUID]database.Template{}
|
templateByID := map[uuid.UUID]database.Template{}
|
||||||
@ -848,3 +849,12 @@ func convertSQLNullInt64(i sql.NullInt64) *time.Duration {
|
|||||||
|
|
||||||
return (*time.Duration)(&i.Int64)
|
return (*time.Duration)(&i.Int64)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func withinDuration(t1, t2 time.Time, d time.Duration) bool {
|
||||||
|
dt := t1.Sub(t2)
|
||||||
|
if dt < -d || dt > d {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user