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)
|
||||
})
|
||||
}
|
87
cli/list.go
87
cli/list.go
@ -86,6 +86,61 @@ func list() *cobra.Command {
|
||||
}
|
||||
|
||||
duration := time.Now().UTC().Sub(workspace.LatestBuild.Job.CreatedAt).Truncate(time.Second)
|
||||
autostartDisplay := "-"
|
||||
if workspace.AutostartSchedule != "" {
|
||||
if sched, err := schedule.Weekly(workspace.AutostartSchedule); err == nil {
|
||||
autostartDisplay = sched.Cron()
|
||||
}
|
||||
}
|
||||
|
||||
autostopDisplay := "-"
|
||||
if workspace.TTL != nil {
|
||||
autostopDisplay = durationDisplay(*workspace.TTL)
|
||||
if has, ext := hasExtension(workspace); has {
|
||||
autostopDisplay += fmt.Sprintf(" (+%s)", durationDisplay(ext.Round(time.Minute)))
|
||||
}
|
||||
}
|
||||
|
||||
user := usersByID[workspace.OwnerID]
|
||||
tableWriter.AppendRow(table.Row{
|
||||
user.Username + "/" + workspace.Name,
|
||||
workspace.TemplateName,
|
||||
status,
|
||||
durationDisplay(duration),
|
||||
workspace.Outdated,
|
||||
autostartDisplay,
|
||||
autostopDisplay,
|
||||
})
|
||||
}
|
||||
_, err = fmt.Fprintln(cmd.OutOrStdout(), tableWriter.Render())
|
||||
return err
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringArrayVarP(&columns, "column", "c", nil,
|
||||
"Specify a column to filter in the table.")
|
||||
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)
|
||||
}
|
||||
@ -107,35 +162,5 @@ func list() *cobra.Command {
|
||||
if strings.HasSuffix(durationDisplay, "h0m") {
|
||||
durationDisplay = durationDisplay[:len(durationDisplay)-2]
|
||||
}
|
||||
|
||||
autostartDisplay := "-"
|
||||
if workspace.AutostartSchedule != "" {
|
||||
if sched, err := schedule.Weekly(workspace.AutostartSchedule); err == nil {
|
||||
autostartDisplay = sched.Cron()
|
||||
}
|
||||
}
|
||||
|
||||
autostopDisplay := "-"
|
||||
if workspace.TTL != nil {
|
||||
autostopDisplay = workspace.TTL.String()
|
||||
}
|
||||
|
||||
user := usersByID[workspace.OwnerID]
|
||||
tableWriter.AppendRow(table.Row{
|
||||
user.Username + "/" + workspace.Name,
|
||||
workspace.TemplateName,
|
||||
status,
|
||||
durationDisplay,
|
||||
workspace.Outdated,
|
||||
autostartDisplay,
|
||||
autostopDisplay,
|
||||
})
|
||||
}
|
||||
_, err = fmt.Fprintln(cmd.OutOrStdout(), tableWriter.Render())
|
||||
return err
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringArrayVarP(&columns, "column", "c", nil,
|
||||
"Specify a column to filter in the table.")
|
||||
return cmd
|
||||
return durationDisplay
|
||||
}
|
||||
|
@ -67,6 +67,7 @@ func Root() *cobra.Command {
|
||||
|
||||
cmd.AddCommand(
|
||||
autostart(),
|
||||
bump(),
|
||||
configSSH(),
|
||||
create(),
|
||||
delete(),
|
||||
|
@ -289,17 +289,14 @@ func notifyCondition(ctx context.Context, client *codersdk.Client, workspaceID u
|
||||
return time.Time{}, nil
|
||||
}
|
||||
|
||||
deadline = ws.LatestBuild.UpdatedAt.Add(*ws.TTL)
|
||||
deadline = ws.LatestBuild.Deadline
|
||||
callback = func() {
|
||||
ttl := deadline.Sub(now)
|
||||
var title, body string
|
||||
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(
|
||||
`Your Coder workspace %s is scheduled to stop at %s.`,
|
||||
ws.Name,
|
||||
deadline.Format(time.Kitchen),
|
||||
)
|
||||
`Your Coder workspace %s is scheduled to stop in %.0f mins`, ws.Name, ttl.Minutes())
|
||||
} else {
|
||||
title = fmt.Sprintf("Workspace %s stopping!", 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 != "" {
|
||||
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("error", priorJob.Error.String),
|
||||
)
|
||||
@ -107,13 +107,14 @@ func (e *Executor) runOnce(t time.Time) error {
|
||||
)
|
||||
continue
|
||||
}
|
||||
// Truncate to nearest minute for consistency with autostart behavior
|
||||
nextTransition = priorHistory.Deadline.Truncate(time.Minute)
|
||||
// For stopping, do not truncate. This is inconsistent with autostart, but
|
||||
// it ensures we will not stop too early.
|
||||
nextTransition = priorHistory.Deadline
|
||||
case database.WorkspaceTransitionStop:
|
||||
validTransition = database.WorkspaceTransitionStart
|
||||
sched, err := schedule.Weekly(ws.AutostartSchedule.String)
|
||||
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("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)
|
||||
if err == nil {
|
||||
if workspace.Ttl.Valid {
|
||||
workspaceDeadline = now.Add(time.Duration(workspace.Ttl.Int64)).Truncate(time.Minute)
|
||||
workspaceDeadline = now.Add(time.Duration(workspace.Ttl.Int64))
|
||||
}
|
||||
} else {
|
||||
// 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)
|
||||
}
|
||||
|
||||
newDeadline := req.Deadline.Truncate(time.Minute).UTC()
|
||||
newDeadline := req.Deadline.UTC()
|
||||
if newDeadline.IsZero() {
|
||||
// This should not be possible because the struct validation field enforces a non-zero value.
|
||||
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))
|
||||
}
|
||||
|
||||
// both newDeadline and build.Deadline are truncated to time.Minute
|
||||
if newDeadline == build.Deadline {
|
||||
// Disallow updates within less than one minute
|
||||
if withinDuration(newDeadline, build.Deadline, time.Minute) {
|
||||
code = http.StatusNotModified
|
||||
return nil
|
||||
}
|
||||
@ -786,6 +786,7 @@ func convertWorkspaces(ctx context.Context, db database.Store, workspaces []data
|
||||
InitiatorID: workspaceBuild.InitiatorID,
|
||||
ProvisionerState: workspaceBuild.ProvisionerState,
|
||||
JobID: workspaceBuild.JobID,
|
||||
Deadline: workspaceBuild.Deadline,
|
||||
}
|
||||
}
|
||||
templateByID := map[uuid.UUID]database.Template{}
|
||||
@ -848,3 +849,12 @@ func convertSQLNullInt64(i sql.NullInt64) *time.Duration {
|
||||
|
||||
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