mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix: do not set max deadline for workspaces on template update (#12446)
* fix: do not set max deadline for workspaces on template update When templates are updated and schedule data is changed, we update all running workspaces to have up-to-date scheduling information that sticks to the new policy. When updating the max_deadline for existing running workspaces, if the max_deadline was before now()+2h we would set the max_deadline to now()+2h. Builds that don't/shouldn't have a max_deadline have it set to 0, which is always before now()+2h, and thus would always have the max_deadline updated. * test: add unit test to excercise template schedule bug --------- Co-authored-by: Steven Masley <stevenmasley@gmail.com>
This commit is contained in:
co-authored by
Steven Masley
parent
17caf58b5e
commit
586586e9dd
@@ -277,15 +277,24 @@ func (s *EnterpriseTemplateScheduleStore) updateWorkspaceBuild(ctx context.Conte
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If max deadline is before now()+2h, then set it to that.
|
// If max deadline is before now()+2h, then set it to that.
|
||||||
|
// This is intended to give ample warning to this workspace about an upcoming auto-stop.
|
||||||
|
// If we were to omit this "grace" period, then this workspace could be set to be stopped "now".
|
||||||
|
// The "2 hours" was an arbitrary decision for this window.
|
||||||
now := s.now()
|
now := s.now()
|
||||||
if autostop.MaxDeadline.Before(now.Add(2 * time.Hour)) {
|
if !autostop.MaxDeadline.IsZero() && autostop.MaxDeadline.Before(now.Add(2*time.Hour)) {
|
||||||
autostop.MaxDeadline = now.Add(time.Hour * 2)
|
autostop.MaxDeadline = now.Add(time.Hour * 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the current deadline on the build is after the new max_deadline, then
|
// If the current deadline on the build is after the new max_deadline, then
|
||||||
// set it to the max_deadline.
|
// set it to the max_deadline.
|
||||||
autostop.Deadline = build.Deadline
|
autostop.Deadline = build.Deadline
|
||||||
if autostop.Deadline.After(autostop.MaxDeadline) {
|
if !autostop.MaxDeadline.IsZero() && autostop.Deadline.After(autostop.MaxDeadline) {
|
||||||
|
autostop.Deadline = autostop.MaxDeadline
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there's a max_deadline but the deadline is 0, then set the deadline to
|
||||||
|
// the max_deadline.
|
||||||
|
if !autostop.MaxDeadline.IsZero() && autostop.Deadline.IsZero() {
|
||||||
autostop.Deadline = autostop.MaxDeadline
|
autostop.Deadline = autostop.MaxDeadline
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
"github.com/coder/coder/v2/coderd/database/dbgen"
|
"github.com/coder/coder/v2/coderd/database/dbgen"
|
||||||
"github.com/coder/coder/v2/coderd/database/dbtestutil"
|
"github.com/coder/coder/v2/coderd/database/dbtestutil"
|
||||||
agplschedule "github.com/coder/coder/v2/coderd/schedule"
|
agplschedule "github.com/coder/coder/v2/coderd/schedule"
|
||||||
|
"github.com/coder/coder/v2/coderd/util/ptr"
|
||||||
"github.com/coder/coder/v2/cryptorand"
|
"github.com/coder/coder/v2/cryptorand"
|
||||||
"github.com/coder/coder/v2/enterprise/coderd/schedule"
|
"github.com/coder/coder/v2/enterprise/coderd/schedule"
|
||||||
"github.com/coder/coder/v2/testutil"
|
"github.com/coder/coder/v2/testutil"
|
||||||
@@ -27,30 +28,35 @@ func TestTemplateUpdateBuildDeadlines(t *testing.T) {
|
|||||||
db, _ := dbtestutil.NewDB(t)
|
db, _ := dbtestutil.NewDB(t)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
org = dbgen.Organization(t, db, database.Organization{})
|
org = dbgen.Organization(t, db, database.Organization{})
|
||||||
user = dbgen.User(t, db, database.User{})
|
quietUser = dbgen.User(t, db, database.User{
|
||||||
|
Username: "quiet",
|
||||||
|
})
|
||||||
|
noQuietUser = dbgen.User(t, db, database.User{
|
||||||
|
Username: "no-quiet",
|
||||||
|
})
|
||||||
file = dbgen.File(t, db, database.File{
|
file = dbgen.File(t, db, database.File{
|
||||||
CreatedBy: user.ID,
|
CreatedBy: quietUser.ID,
|
||||||
})
|
})
|
||||||
templateJob = dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
|
templateJob = dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
|
||||||
OrganizationID: org.ID,
|
OrganizationID: org.ID,
|
||||||
FileID: file.ID,
|
FileID: file.ID,
|
||||||
InitiatorID: user.ID,
|
InitiatorID: quietUser.ID,
|
||||||
Tags: database.StringMap{
|
Tags: database.StringMap{
|
||||||
"foo": "bar",
|
"foo": "bar",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
templateVersion = dbgen.TemplateVersion(t, db, database.TemplateVersion{
|
templateVersion = dbgen.TemplateVersion(t, db, database.TemplateVersion{
|
||||||
OrganizationID: org.ID,
|
OrganizationID: org.ID,
|
||||||
CreatedBy: user.ID,
|
CreatedBy: quietUser.ID,
|
||||||
JobID: templateJob.ID,
|
JobID: templateJob.ID,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
const userQuietHoursSchedule = "CRON_TZ=UTC 0 0 * * *" // midnight UTC
|
const userQuietHoursSchedule = "CRON_TZ=UTC 0 0 * * *" // midnight UTC
|
||||||
ctx := testutil.Context(t, testutil.WaitLong)
|
ctx := testutil.Context(t, testutil.WaitLong)
|
||||||
user, err := db.UpdateUserQuietHoursSchedule(ctx, database.UpdateUserQuietHoursScheduleParams{
|
quietUser, err := db.UpdateUserQuietHoursSchedule(ctx, database.UpdateUserQuietHoursScheduleParams{
|
||||||
ID: user.ID,
|
ID: quietUser.ID,
|
||||||
QuietHoursSchedule: userQuietHoursSchedule,
|
QuietHoursSchedule: userQuietHoursSchedule,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -62,12 +68,15 @@ func TestTemplateUpdateBuildDeadlines(t *testing.T) {
|
|||||||
|
|
||||||
// Workspace old max_deadline too soon
|
// Workspace old max_deadline too soon
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
name string
|
name string
|
||||||
now time.Time
|
now time.Time
|
||||||
deadline time.Time
|
deadline time.Time
|
||||||
maxDeadline time.Time
|
maxDeadline time.Time
|
||||||
newDeadline time.Time // 0 for no change
|
// Set to nil for no change.
|
||||||
|
newDeadline *time.Time
|
||||||
newMaxDeadline time.Time
|
newMaxDeadline time.Time
|
||||||
|
noQuietHours bool
|
||||||
|
autostopReq *agplschedule.TemplateAutostopRequirement
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "SkippedWorkspaceMaxDeadlineTooSoon",
|
name: "SkippedWorkspaceMaxDeadlineTooSoon",
|
||||||
@@ -75,7 +84,7 @@ func TestTemplateUpdateBuildDeadlines(t *testing.T) {
|
|||||||
deadline: buildTime,
|
deadline: buildTime,
|
||||||
maxDeadline: buildTime.Add(1 * time.Hour),
|
maxDeadline: buildTime.Add(1 * time.Hour),
|
||||||
// Unchanged since the max deadline is too soon.
|
// Unchanged since the max deadline is too soon.
|
||||||
newDeadline: time.Time{},
|
newDeadline: nil,
|
||||||
newMaxDeadline: buildTime.Add(1 * time.Hour),
|
newMaxDeadline: buildTime.Add(1 * time.Hour),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -85,7 +94,7 @@ func TestTemplateUpdateBuildDeadlines(t *testing.T) {
|
|||||||
deadline: buildTime,
|
deadline: buildTime,
|
||||||
// Far into the future...
|
// Far into the future...
|
||||||
maxDeadline: nextQuietHours.Add(24 * time.Hour),
|
maxDeadline: nextQuietHours.Add(24 * time.Hour),
|
||||||
newDeadline: time.Time{},
|
newDeadline: nil,
|
||||||
// We will use now() + 2 hours if the newly calculated max deadline
|
// We will use now() + 2 hours if the newly calculated max deadline
|
||||||
// from the workspace build time is before now.
|
// from the workspace build time is before now.
|
||||||
newMaxDeadline: nextQuietHours.Add(8 * time.Hour),
|
newMaxDeadline: nextQuietHours.Add(8 * time.Hour),
|
||||||
@@ -97,7 +106,7 @@ func TestTemplateUpdateBuildDeadlines(t *testing.T) {
|
|||||||
deadline: buildTime,
|
deadline: buildTime,
|
||||||
// Far into the future...
|
// Far into the future...
|
||||||
maxDeadline: nextQuietHours.Add(24 * time.Hour),
|
maxDeadline: nextQuietHours.Add(24 * time.Hour),
|
||||||
newDeadline: time.Time{},
|
newDeadline: nil,
|
||||||
// We will use now() + 2 hours if the newly calculated max deadline
|
// We will use now() + 2 hours if the newly calculated max deadline
|
||||||
// from the workspace build time is within the next 2 hours.
|
// from the workspace build time is within the next 2 hours.
|
||||||
newMaxDeadline: nextQuietHours.Add(1 * time.Hour),
|
newMaxDeadline: nextQuietHours.Add(1 * time.Hour),
|
||||||
@@ -109,7 +118,7 @@ func TestTemplateUpdateBuildDeadlines(t *testing.T) {
|
|||||||
deadline: buildTime,
|
deadline: buildTime,
|
||||||
// Far into the future...
|
// Far into the future...
|
||||||
maxDeadline: nextQuietHours.Add(24 * time.Hour),
|
maxDeadline: nextQuietHours.Add(24 * time.Hour),
|
||||||
newDeadline: time.Time{},
|
newDeadline: nil,
|
||||||
newMaxDeadline: nextQuietHours,
|
newMaxDeadline: nextQuietHours,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -120,7 +129,56 @@ func TestTemplateUpdateBuildDeadlines(t *testing.T) {
|
|||||||
deadline: nextQuietHours.Add(24 * time.Hour),
|
deadline: nextQuietHours.Add(24 * time.Hour),
|
||||||
maxDeadline: nextQuietHours.Add(24 * time.Hour),
|
maxDeadline: nextQuietHours.Add(24 * time.Hour),
|
||||||
// The deadline should match since it is after the new max deadline.
|
// The deadline should match since it is after the new max deadline.
|
||||||
newDeadline: nextQuietHours,
|
newDeadline: ptr.Ref(nextQuietHours),
|
||||||
|
newMaxDeadline: nextQuietHours,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// There was a bug if a user has no quiet hours set, and autostop
|
||||||
|
// req is not turned on, then the max deadline is set to `time.Time{}`.
|
||||||
|
// This zero value was "in the past", so the workspace deadline would
|
||||||
|
// be set to "now" + 2 hours.
|
||||||
|
// This is a mistake because the max deadline being zero means
|
||||||
|
// there is no max deadline.
|
||||||
|
name: "MaxDeadlineShouldBeUnset",
|
||||||
|
now: buildTime,
|
||||||
|
deadline: buildTime.Add(time.Hour * 8),
|
||||||
|
maxDeadline: time.Time{}, // No max set
|
||||||
|
// Should be unchanged
|
||||||
|
newDeadline: ptr.Ref(buildTime.Add(time.Hour * 8)),
|
||||||
|
newMaxDeadline: time.Time{},
|
||||||
|
noQuietHours: true,
|
||||||
|
autostopReq: &agplschedule.TemplateAutostopRequirement{
|
||||||
|
DaysOfWeek: 0,
|
||||||
|
Weeks: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// A bug existed where MaxDeadline could be set, but deadline was
|
||||||
|
// `time.Time{}`. This is a logical inconsistency because the "max"
|
||||||
|
// deadline was ignored.
|
||||||
|
name: "NoDeadline",
|
||||||
|
now: buildTime,
|
||||||
|
deadline: time.Time{},
|
||||||
|
maxDeadline: time.Time{}, // No max set
|
||||||
|
// Should be unchanged
|
||||||
|
newDeadline: ptr.Ref(time.Time{}),
|
||||||
|
newMaxDeadline: time.Time{},
|
||||||
|
noQuietHours: true,
|
||||||
|
autostopReq: &agplschedule.TemplateAutostopRequirement{
|
||||||
|
DaysOfWeek: 0,
|
||||||
|
Weeks: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
// Similar to 'NoDeadline' test. This has a MaxDeadline set, so
|
||||||
|
// the deadline of the workspace should now be set.
|
||||||
|
name: "WorkspaceDeadlineNowSet",
|
||||||
|
now: nextQuietHours.Add(-6 * time.Hour),
|
||||||
|
// Start with unset times
|
||||||
|
deadline: time.Time{},
|
||||||
|
maxDeadline: time.Time{},
|
||||||
|
newDeadline: ptr.Ref(nextQuietHours),
|
||||||
newMaxDeadline: nextQuietHours,
|
newMaxDeadline: nextQuietHours,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -131,6 +189,11 @@ func TestTemplateUpdateBuildDeadlines(t *testing.T) {
|
|||||||
t.Run(c.name, func(t *testing.T) {
|
t.Run(c.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
user := quietUser
|
||||||
|
if c.noQuietHours {
|
||||||
|
user = noQuietUser
|
||||||
|
}
|
||||||
|
|
||||||
t.Log("buildTime", buildTime)
|
t.Log("buildTime", buildTime)
|
||||||
t.Log("nextQuietHours", nextQuietHours)
|
t.Log("nextQuietHours", nextQuietHours)
|
||||||
t.Log("now", c.now)
|
t.Log("now", c.now)
|
||||||
@@ -217,17 +280,22 @@ func TestTemplateUpdateBuildDeadlines(t *testing.T) {
|
|||||||
templateScheduleStore.TimeNowFn = func() time.Time {
|
templateScheduleStore.TimeNowFn = func() time.Time {
|
||||||
return c.now
|
return c.now
|
||||||
}
|
}
|
||||||
|
|
||||||
|
autostopReq := agplschedule.TemplateAutostopRequirement{
|
||||||
|
// Every day
|
||||||
|
DaysOfWeek: 0b01111111,
|
||||||
|
Weeks: 0,
|
||||||
|
}
|
||||||
|
if c.autostopReq != nil {
|
||||||
|
autostopReq = *c.autostopReq
|
||||||
|
}
|
||||||
_, err = templateScheduleStore.Set(ctx, db, template, agplschedule.TemplateScheduleOptions{
|
_, err = templateScheduleStore.Set(ctx, db, template, agplschedule.TemplateScheduleOptions{
|
||||||
UserAutostartEnabled: false,
|
UserAutostartEnabled: false,
|
||||||
UserAutostopEnabled: false,
|
UserAutostopEnabled: false,
|
||||||
DefaultTTL: 0,
|
DefaultTTL: 0,
|
||||||
MaxTTL: 0,
|
MaxTTL: 0,
|
||||||
UseMaxTTL: false,
|
UseMaxTTL: false,
|
||||||
AutostopRequirement: agplschedule.TemplateAutostopRequirement{
|
AutostopRequirement: autostopReq,
|
||||||
// Every day
|
|
||||||
DaysOfWeek: 0b01111111,
|
|
||||||
Weeks: 0,
|
|
||||||
},
|
|
||||||
FailureTTL: 0,
|
FailureTTL: 0,
|
||||||
TimeTilDormant: 0,
|
TimeTilDormant: 0,
|
||||||
TimeTilDormantAutoDelete: 0,
|
TimeTilDormantAutoDelete: 0,
|
||||||
@@ -238,10 +306,10 @@ func TestTemplateUpdateBuildDeadlines(t *testing.T) {
|
|||||||
newBuild, err := db.GetWorkspaceBuildByID(ctx, wsBuild.ID)
|
newBuild, err := db.GetWorkspaceBuildByID(ctx, wsBuild.ID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
if c.newDeadline.IsZero() {
|
if c.newDeadline == nil {
|
||||||
c.newDeadline = wsBuild.Deadline
|
c.newDeadline = &wsBuild.Deadline
|
||||||
}
|
}
|
||||||
require.WithinDuration(t, c.newDeadline, newBuild.Deadline, time.Second)
|
require.WithinDuration(t, *c.newDeadline, newBuild.Deadline, time.Second)
|
||||||
require.WithinDuration(t, c.newMaxDeadline, newBuild.MaxDeadline, time.Second)
|
require.WithinDuration(t, c.newMaxDeadline, newBuild.MaxDeadline, time.Second)
|
||||||
|
|
||||||
// Check that the new build has the same state as before.
|
// Check that the new build has the same state as before.
|
||||||
|
|||||||
Reference in New Issue
Block a user