mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
fix: make GetWorkspacesEligibleForTransition return even less false positives (#15594)
Relates to https://github.com/coder/coder/issues/15082 Further to https://github.com/coder/coder/pull/15429, this reduces the amount of false-positives returned by the 'is eligible for autostart' part of the query. We achieve this by calculating the 'next start at' time of the workspace, storing it in the database, and using it in our `GetWorkspacesEligibleForTransition` query. The prior implementation of the 'is eligible for autostart' query would return _all_ workspaces that at some point in the future _might_ be eligible for autostart. This now ensures we only return workspaces that _should_ be eligible for autostart. We also now pass `currentTick` instead of `t` to the `GetWorkspacesEligibleForTransition` query as otherwise we'll have one round of workspaces that are skipped by `isEligibleForTransition` due to `currentTick` being a truncated version of `t`.
This commit is contained in:
+2
-1
@@ -65,6 +65,7 @@
|
||||
},
|
||||
"automatic_updates": "never",
|
||||
"allow_renames": false,
|
||||
"favorite": false
|
||||
"favorite": false,
|
||||
"next_start_at": "[timestamp]"
|
||||
}
|
||||
]
|
||||
|
||||
Generated
+4
@@ -14543,6 +14543,10 @@ const docTemplate = `{
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"next_start_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"organization_id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
|
||||
Generated
+4
@@ -13218,6 +13218,10 @@
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"next_start_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"organization_id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
|
||||
@@ -142,7 +142,7 @@ func (e *Executor) runOnce(t time.Time) Stats {
|
||||
// NOTE: If a workspace build is created with a given TTL and then the user either
|
||||
// changes or unsets the TTL, the deadline for the workspace build will not
|
||||
// have changed. This behavior is as expected per #2229.
|
||||
workspaces, err := e.db.GetWorkspacesEligibleForTransition(e.ctx, t)
|
||||
workspaces, err := e.db.GetWorkspacesEligibleForTransition(e.ctx, currentTick)
|
||||
if err != nil {
|
||||
e.log.Error(e.ctx, "get workspaces for autostart or autostop", slog.Error(err))
|
||||
return stats
|
||||
@@ -205,6 +205,23 @@ func (e *Executor) runOnce(t time.Time) Stats {
|
||||
return xerrors.Errorf("get template scheduling options: %w", err)
|
||||
}
|
||||
|
||||
// If next start at is not valid we need to re-compute it
|
||||
if !ws.NextStartAt.Valid && ws.AutostartSchedule.Valid {
|
||||
next, err := schedule.NextAllowedAutostart(currentTick, ws.AutostartSchedule.String, templateSchedule)
|
||||
if err == nil {
|
||||
nextStartAt := sql.NullTime{Valid: true, Time: dbtime.Time(next.UTC())}
|
||||
if err = tx.UpdateWorkspaceNextStartAt(e.ctx, database.UpdateWorkspaceNextStartAtParams{
|
||||
ID: wsID,
|
||||
NextStartAt: nextStartAt,
|
||||
}); err != nil {
|
||||
return xerrors.Errorf("update workspace next start at: %w", err)
|
||||
}
|
||||
|
||||
// Save re-fetching the workspace
|
||||
ws.NextStartAt = nextStartAt
|
||||
}
|
||||
}
|
||||
|
||||
tmpl, err = tx.GetTemplateByID(e.ctx, ws.TemplateID)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get template by ID: %w", err)
|
||||
@@ -463,8 +480,8 @@ func isEligibleForAutostart(user database.User, ws database.Workspace, build dat
|
||||
return false
|
||||
}
|
||||
|
||||
nextTransition, allowed := schedule.NextAutostart(build.CreatedAt, ws.AutostartSchedule.String, templateSchedule)
|
||||
if !allowed {
|
||||
nextTransition, err := schedule.NextAllowedAutostart(build.CreatedAt, ws.AutostartSchedule.String, templateSchedule)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -1083,6 +1083,10 @@ func TestNotifications(t *testing.T) {
|
||||
IncludeProvisionerDaemon: true,
|
||||
NotificationsEnqueuer: ¬ifyEnq,
|
||||
TemplateScheduleStore: schedule.MockTemplateScheduleStore{
|
||||
SetFn: func(ctx context.Context, db database.Store, template database.Template, options schedule.TemplateScheduleOptions) (database.Template, error) {
|
||||
template.TimeTilDormant = int64(options.TimeTilDormant)
|
||||
return schedule.NewAGPLTemplateScheduleStore().Set(ctx, db, template, options)
|
||||
},
|
||||
GetFn: func(_ context.Context, _ database.Store, _ uuid.UUID) (schedule.TemplateScheduleOptions, error) {
|
||||
return schedule.TemplateScheduleOptions{
|
||||
UserAutostartEnabled: false,
|
||||
@@ -1099,7 +1103,9 @@ func TestNotifications(t *testing.T) {
|
||||
)
|
||||
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||
template := coderdtest.CreateTemplate(t, client, admin.OrganizationID, version.ID)
|
||||
template := coderdtest.CreateTemplate(t, client, admin.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
|
||||
ctr.TimeTilDormantMillis = ptr.Ref(timeTilDormant.Milliseconds())
|
||||
})
|
||||
userClient, _ := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID)
|
||||
workspace := coderdtest.CreateWorkspace(t, userClient, template.ID)
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, workspace.LatestBuild.ID)
|
||||
|
||||
@@ -1053,6 +1053,13 @@ func (q *querier) BatchUpdateWorkspaceLastUsedAt(ctx context.Context, arg databa
|
||||
return q.db.BatchUpdateWorkspaceLastUsedAt(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) BatchUpdateWorkspaceNextStartAt(ctx context.Context, arg database.BatchUpdateWorkspaceNextStartAtParams) error {
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceWorkspace.All()); err != nil {
|
||||
return err
|
||||
}
|
||||
return q.db.BatchUpdateWorkspaceNextStartAt(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) BulkMarkNotificationMessagesFailed(ctx context.Context, arg database.BulkMarkNotificationMessagesFailedParams) (int64, error) {
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceNotificationMessage); err != nil {
|
||||
return 0, err
|
||||
@@ -2840,6 +2847,13 @@ func (q *querier) GetWorkspacesAndAgentsByOwnerID(ctx context.Context, ownerID u
|
||||
return q.db.GetAuthorizedWorkspacesAndAgentsByOwnerID(ctx, ownerID, prep)
|
||||
}
|
||||
|
||||
func (q *querier) GetWorkspacesByTemplateID(ctx context.Context, templateID uuid.UUID) ([]database.WorkspaceTable, error) {
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return q.db.GetWorkspacesByTemplateID(ctx, templateID)
|
||||
}
|
||||
|
||||
func (q *querier) GetWorkspacesEligibleForTransition(ctx context.Context, now time.Time) ([]database.GetWorkspacesEligibleForTransitionRow, error) {
|
||||
return q.db.GetWorkspacesEligibleForTransition(ctx, now)
|
||||
}
|
||||
@@ -4085,6 +4099,13 @@ func (q *querier) UpdateWorkspaceLastUsedAt(ctx context.Context, arg database.Up
|
||||
return update(q.log, q.auth, fetch, q.db.UpdateWorkspaceLastUsedAt)(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateWorkspaceNextStartAt(ctx context.Context, arg database.UpdateWorkspaceNextStartAtParams) error {
|
||||
fetch := func(ctx context.Context, arg database.UpdateWorkspaceNextStartAtParams) (database.Workspace, error) {
|
||||
return q.db.GetWorkspaceByID(ctx, arg.ID)
|
||||
}
|
||||
return update(q.log, q.auth, fetch, q.db.UpdateWorkspaceNextStartAt)(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateWorkspaceProxy(ctx context.Context, arg database.UpdateWorkspaceProxyParams) (database.WorkspaceProxy, error) {
|
||||
fetch := func(ctx context.Context, arg database.UpdateWorkspaceProxyParams) (database.WorkspaceProxy, error) {
|
||||
return q.db.GetWorkspaceProxyByID(ctx, arg.ID)
|
||||
|
||||
@@ -1908,6 +1908,19 @@ func (s *MethodTestSuite) TestWorkspace() {
|
||||
ID: ws.ID,
|
||||
}).Asserts(ws, policy.ActionUpdate).Returns()
|
||||
}))
|
||||
s.Run("UpdateWorkspaceNextStartAt", s.Subtest(func(db database.Store, check *expects) {
|
||||
ws := dbgen.Workspace(s.T(), db, database.WorkspaceTable{})
|
||||
check.Args(database.UpdateWorkspaceNextStartAtParams{
|
||||
ID: ws.ID,
|
||||
NextStartAt: sql.NullTime{Valid: true, Time: dbtime.Now()},
|
||||
}).Asserts(ws, policy.ActionUpdate)
|
||||
}))
|
||||
s.Run("BatchUpdateWorkspaceNextStartAt", s.Subtest(func(db database.Store, check *expects) {
|
||||
check.Args(database.BatchUpdateWorkspaceNextStartAtParams{
|
||||
IDs: []uuid.UUID{uuid.New()},
|
||||
NextStartAts: []time.Time{dbtime.Now()},
|
||||
}).Asserts(rbac.ResourceWorkspace.All(), policy.ActionUpdate)
|
||||
}))
|
||||
s.Run("BatchUpdateWorkspaceLastUsedAt", s.Subtest(func(db database.Store, check *expects) {
|
||||
ws1 := dbgen.Workspace(s.T(), db, database.WorkspaceTable{})
|
||||
ws2 := dbgen.Workspace(s.T(), db, database.WorkspaceTable{})
|
||||
@@ -2784,6 +2797,9 @@ func (s *MethodTestSuite) TestSystemFunctions() {
|
||||
s.Run("GetTemplateAverageBuildTime", s.Subtest(func(db database.Store, check *expects) {
|
||||
check.Args(database.GetTemplateAverageBuildTimeParams{}).Asserts(rbac.ResourceSystem, policy.ActionRead)
|
||||
}))
|
||||
s.Run("GetWorkspacesByTemplateID", s.Subtest(func(db database.Store, check *expects) {
|
||||
check.Args(uuid.Nil).Asserts(rbac.ResourceSystem, policy.ActionRead)
|
||||
}))
|
||||
s.Run("GetWorkspacesEligibleForTransition", s.Subtest(func(db database.Store, check *expects) {
|
||||
check.Args(time.Time{}).Asserts()
|
||||
}))
|
||||
|
||||
@@ -260,6 +260,7 @@ func Workspace(t testing.TB, db database.Store, orig database.WorkspaceTable) da
|
||||
AutostartSchedule: orig.AutostartSchedule,
|
||||
Ttl: orig.Ttl,
|
||||
AutomaticUpdates: takeFirst(orig.AutomaticUpdates, database.AutomaticUpdatesNever),
|
||||
NextStartAt: orig.NextStartAt,
|
||||
})
|
||||
require.NoError(t, err, "insert workspace")
|
||||
return workspace
|
||||
|
||||
@@ -475,6 +475,7 @@ func (q *FakeQuerier) convertToWorkspaceRowsNoLock(ctx context.Context, workspac
|
||||
DeletingAt: w.DeletingAt,
|
||||
AutomaticUpdates: w.AutomaticUpdates,
|
||||
Favorite: w.Favorite,
|
||||
NextStartAt: w.NextStartAt,
|
||||
|
||||
OwnerAvatarUrl: extended.OwnerAvatarUrl,
|
||||
OwnerUsername: extended.OwnerUsername,
|
||||
@@ -1431,6 +1432,35 @@ func (q *FakeQuerier) BatchUpdateWorkspaceLastUsedAt(_ context.Context, arg data
|
||||
return nil
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) BatchUpdateWorkspaceNextStartAt(_ context.Context, arg database.BatchUpdateWorkspaceNextStartAtParams) error {
|
||||
err := validateDatabaseType(arg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
||||
for i, workspace := range q.workspaces {
|
||||
for j, workspaceID := range arg.IDs {
|
||||
if workspace.ID != workspaceID {
|
||||
continue
|
||||
}
|
||||
|
||||
nextStartAt := arg.NextStartAts[j]
|
||||
if nextStartAt.IsZero() {
|
||||
q.workspaces[i].NextStartAt = sql.NullTime{}
|
||||
} else {
|
||||
q.workspaces[i].NextStartAt = sql.NullTime{Valid: true, Time: nextStartAt}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*FakeQuerier) BulkMarkNotificationMessagesFailed(_ context.Context, arg database.BulkMarkNotificationMessagesFailedParams) (int64, error) {
|
||||
err := validateDatabaseType(arg)
|
||||
if err != nil {
|
||||
@@ -6908,6 +6938,20 @@ func (q *FakeQuerier) GetWorkspacesAndAgentsByOwnerID(ctx context.Context, owner
|
||||
return q.GetAuthorizedWorkspacesAndAgentsByOwnerID(ctx, ownerID, nil)
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) GetWorkspacesByTemplateID(_ context.Context, templateID uuid.UUID) ([]database.WorkspaceTable, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
workspaces := []database.WorkspaceTable{}
|
||||
for _, workspace := range q.workspaces {
|
||||
if workspace.TemplateID == templateID {
|
||||
workspaces = append(workspaces, workspace)
|
||||
}
|
||||
}
|
||||
|
||||
return workspaces, nil
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) GetWorkspacesEligibleForTransition(ctx context.Context, now time.Time) ([]database.GetWorkspacesEligibleForTransitionRow, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
@@ -6952,7 +6996,13 @@ func (q *FakeQuerier) GetWorkspacesEligibleForTransition(ctx context.Context, no
|
||||
if user.Status == database.UserStatusActive &&
|
||||
job.JobStatus != database.ProvisionerJobStatusFailed &&
|
||||
build.Transition == database.WorkspaceTransitionStop &&
|
||||
workspace.AutostartSchedule.Valid {
|
||||
workspace.AutostartSchedule.Valid &&
|
||||
// We do not know if workspace with a zero next start is eligible
|
||||
// for autostart, so we accept this false-positive. This can occur
|
||||
// when a coder version is upgraded and next_start_at has yet to
|
||||
// be set.
|
||||
(workspace.NextStartAt.Time.IsZero() ||
|
||||
!now.Before(workspace.NextStartAt.Time)) {
|
||||
workspaces = append(workspaces, database.GetWorkspacesEligibleForTransitionRow{
|
||||
ID: workspace.ID,
|
||||
Name: workspace.Name,
|
||||
@@ -6962,7 +7012,7 @@ func (q *FakeQuerier) GetWorkspacesEligibleForTransition(ctx context.Context, no
|
||||
|
||||
if !workspace.DormantAt.Valid &&
|
||||
template.TimeTilDormant > 0 &&
|
||||
now.Sub(workspace.LastUsedAt) > time.Duration(template.TimeTilDormant) {
|
||||
now.Sub(workspace.LastUsedAt) >= time.Duration(template.TimeTilDormant) {
|
||||
workspaces = append(workspaces, database.GetWorkspacesEligibleForTransitionRow{
|
||||
ID: workspace.ID,
|
||||
Name: workspace.Name,
|
||||
@@ -7927,6 +7977,7 @@ func (q *FakeQuerier) InsertWorkspace(_ context.Context, arg database.InsertWork
|
||||
Ttl: arg.Ttl,
|
||||
LastUsedAt: arg.LastUsedAt,
|
||||
AutomaticUpdates: arg.AutomaticUpdates,
|
||||
NextStartAt: arg.NextStartAt,
|
||||
}
|
||||
q.workspaces = append(q.workspaces, workspace)
|
||||
return workspace, nil
|
||||
@@ -9868,6 +9919,7 @@ func (q *FakeQuerier) UpdateWorkspaceAutostart(_ context.Context, arg database.U
|
||||
continue
|
||||
}
|
||||
workspace.AutostartSchedule = arg.AutostartSchedule
|
||||
workspace.NextStartAt = arg.NextStartAt
|
||||
q.workspaces[index] = workspace
|
||||
return nil
|
||||
}
|
||||
@@ -10017,6 +10069,29 @@ func (q *FakeQuerier) UpdateWorkspaceLastUsedAt(_ context.Context, arg database.
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) UpdateWorkspaceNextStartAt(_ context.Context, arg database.UpdateWorkspaceNextStartAtParams) error {
|
||||
err := validateDatabaseType(arg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
||||
for index, workspace := range q.workspaces {
|
||||
if workspace.ID != arg.ID {
|
||||
continue
|
||||
}
|
||||
|
||||
workspace.NextStartAt = arg.NextStartAt
|
||||
q.workspaces[index] = workspace
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) UpdateWorkspaceProxy(_ context.Context, arg database.UpdateWorkspaceProxyParams) (database.WorkspaceProxy, error) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
||||
@@ -126,6 +126,13 @@ func (m queryMetricsStore) BatchUpdateWorkspaceLastUsedAt(ctx context.Context, a
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) BatchUpdateWorkspaceNextStartAt(ctx context.Context, arg database.BatchUpdateWorkspaceNextStartAtParams) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.BatchUpdateWorkspaceNextStartAt(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("BatchUpdateWorkspaceNextStartAt").Observe(time.Since(start).Seconds())
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) BulkMarkNotificationMessagesFailed(ctx context.Context, arg database.BulkMarkNotificationMessagesFailedParams) (int64, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.BulkMarkNotificationMessagesFailed(ctx, arg)
|
||||
@@ -1673,6 +1680,13 @@ func (m queryMetricsStore) GetWorkspacesAndAgentsByOwnerID(ctx context.Context,
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetWorkspacesByTemplateID(ctx context.Context, templateID uuid.UUID) ([]database.WorkspaceTable, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetWorkspacesByTemplateID(ctx, templateID)
|
||||
m.queryLatencies.WithLabelValues("GetWorkspacesByTemplateID").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetWorkspacesEligibleForTransition(ctx context.Context, now time.Time) ([]database.GetWorkspacesEligibleForTransitionRow, error) {
|
||||
start := time.Now()
|
||||
workspaces, err := m.s.GetWorkspacesEligibleForTransition(ctx, now)
|
||||
@@ -2541,6 +2555,13 @@ func (m queryMetricsStore) UpdateWorkspaceLastUsedAt(ctx context.Context, arg da
|
||||
return err
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdateWorkspaceNextStartAt(ctx context.Context, arg database.UpdateWorkspaceNextStartAtParams) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.UpdateWorkspaceNextStartAt(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("UpdateWorkspaceNextStartAt").Observe(time.Since(start).Seconds())
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdateWorkspaceProxy(ctx context.Context, arg database.UpdateWorkspaceProxyParams) (database.WorkspaceProxy, error) {
|
||||
start := time.Now()
|
||||
proxy, err := m.s.UpdateWorkspaceProxy(ctx, arg)
|
||||
|
||||
@@ -145,6 +145,20 @@ func (mr *MockStoreMockRecorder) BatchUpdateWorkspaceLastUsedAt(arg0, arg1 any)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BatchUpdateWorkspaceLastUsedAt", reflect.TypeOf((*MockStore)(nil).BatchUpdateWorkspaceLastUsedAt), arg0, arg1)
|
||||
}
|
||||
|
||||
// BatchUpdateWorkspaceNextStartAt mocks base method.
|
||||
func (m *MockStore) BatchUpdateWorkspaceNextStartAt(arg0 context.Context, arg1 database.BatchUpdateWorkspaceNextStartAtParams) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "BatchUpdateWorkspaceNextStartAt", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// BatchUpdateWorkspaceNextStartAt indicates an expected call of BatchUpdateWorkspaceNextStartAt.
|
||||
func (mr *MockStoreMockRecorder) BatchUpdateWorkspaceNextStartAt(arg0, arg1 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BatchUpdateWorkspaceNextStartAt", reflect.TypeOf((*MockStore)(nil).BatchUpdateWorkspaceNextStartAt), arg0, arg1)
|
||||
}
|
||||
|
||||
// BulkMarkNotificationMessagesFailed mocks base method.
|
||||
func (m *MockStore) BulkMarkNotificationMessagesFailed(arg0 context.Context, arg1 database.BulkMarkNotificationMessagesFailedParams) (int64, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -3532,6 +3546,21 @@ func (mr *MockStoreMockRecorder) GetWorkspacesAndAgentsByOwnerID(arg0, arg1 any)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspacesAndAgentsByOwnerID", reflect.TypeOf((*MockStore)(nil).GetWorkspacesAndAgentsByOwnerID), arg0, arg1)
|
||||
}
|
||||
|
||||
// GetWorkspacesByTemplateID mocks base method.
|
||||
func (m *MockStore) GetWorkspacesByTemplateID(arg0 context.Context, arg1 uuid.UUID) ([]database.WorkspaceTable, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetWorkspacesByTemplateID", arg0, arg1)
|
||||
ret0, _ := ret[0].([]database.WorkspaceTable)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetWorkspacesByTemplateID indicates an expected call of GetWorkspacesByTemplateID.
|
||||
func (mr *MockStoreMockRecorder) GetWorkspacesByTemplateID(arg0, arg1 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspacesByTemplateID", reflect.TypeOf((*MockStore)(nil).GetWorkspacesByTemplateID), arg0, arg1)
|
||||
}
|
||||
|
||||
// GetWorkspacesEligibleForTransition mocks base method.
|
||||
func (m *MockStore) GetWorkspacesEligibleForTransition(arg0 context.Context, arg1 time.Time) ([]database.GetWorkspacesEligibleForTransitionRow, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -5385,6 +5414,20 @@ func (mr *MockStoreMockRecorder) UpdateWorkspaceLastUsedAt(arg0, arg1 any) *gomo
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceLastUsedAt", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceLastUsedAt), arg0, arg1)
|
||||
}
|
||||
|
||||
// UpdateWorkspaceNextStartAt mocks base method.
|
||||
func (m *MockStore) UpdateWorkspaceNextStartAt(arg0 context.Context, arg1 database.UpdateWorkspaceNextStartAtParams) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateWorkspaceNextStartAt", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// UpdateWorkspaceNextStartAt indicates an expected call of UpdateWorkspaceNextStartAt.
|
||||
func (mr *MockStoreMockRecorder) UpdateWorkspaceNextStartAt(arg0, arg1 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceNextStartAt", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceNextStartAt), arg0, arg1)
|
||||
}
|
||||
|
||||
// UpdateWorkspaceProxy mocks base method.
|
||||
func (m *MockStore) UpdateWorkspaceProxy(arg0 context.Context, arg1 database.UpdateWorkspaceProxyParams) (database.WorkspaceProxy, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
Generated
+28
-1
@@ -380,6 +380,25 @@ BEGIN
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE FUNCTION nullify_next_start_at_on_workspace_autostart_modification() RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
BEGIN
|
||||
-- A workspace's next_start_at might be invalidated by the following:
|
||||
-- * The autostart schedule has changed independent to next_start_at
|
||||
-- * The workspace has been marked as dormant
|
||||
IF (NEW.autostart_schedule <> OLD.autostart_schedule AND NEW.next_start_at = OLD.next_start_at)
|
||||
OR (NEW.dormant_at IS NOT NULL AND NEW.next_start_at IS NOT NULL)
|
||||
THEN
|
||||
UPDATE workspaces
|
||||
SET next_start_at = NULL
|
||||
WHERE id = NEW.id;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE FUNCTION provisioner_tagset_contains(provisioner_tags tagset, job_tags tagset) RETURNS boolean
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
@@ -1731,7 +1750,8 @@ CREATE TABLE workspaces (
|
||||
dormant_at timestamp with time zone,
|
||||
deleting_at timestamp with time zone,
|
||||
automatic_updates automatic_updates DEFAULT 'never'::automatic_updates NOT NULL,
|
||||
favorite boolean DEFAULT false NOT NULL
|
||||
favorite boolean DEFAULT false NOT NULL,
|
||||
next_start_at timestamp with time zone
|
||||
);
|
||||
|
||||
COMMENT ON COLUMN workspaces.favorite IS 'Favorite is true if the workspace owner has favorited the workspace.';
|
||||
@@ -1752,6 +1772,7 @@ CREATE VIEW workspaces_expanded AS
|
||||
workspaces.deleting_at,
|
||||
workspaces.automatic_updates,
|
||||
workspaces.favorite,
|
||||
workspaces.next_start_at,
|
||||
visible_users.avatar_url AS owner_avatar_url,
|
||||
visible_users.username AS owner_username,
|
||||
organizations.name AS organization_name,
|
||||
@@ -2110,10 +2131,14 @@ CREATE INDEX workspace_app_stats_workspace_id_idx ON workspace_app_stats USING b
|
||||
|
||||
CREATE INDEX workspace_modules_created_at_idx ON workspace_modules USING btree (created_at);
|
||||
|
||||
CREATE INDEX workspace_next_start_at_idx ON workspaces USING btree (next_start_at) WHERE (deleted = false);
|
||||
|
||||
CREATE UNIQUE INDEX workspace_proxies_lower_name_idx ON workspace_proxies USING btree (lower(name)) WHERE (deleted = false);
|
||||
|
||||
CREATE INDEX workspace_resources_job_id_idx ON workspace_resources USING btree (job_id);
|
||||
|
||||
CREATE INDEX workspace_template_id_idx ON workspaces USING btree (template_id) WHERE (deleted = false);
|
||||
|
||||
CREATE UNIQUE INDEX workspaces_owner_id_lower_idx ON workspaces USING btree (owner_id, lower((name)::text)) WHERE (deleted = false);
|
||||
|
||||
CREATE OR REPLACE VIEW provisioner_job_stats AS
|
||||
@@ -2192,6 +2217,8 @@ CREATE TRIGGER trigger_delete_oauth2_provider_app_token AFTER DELETE ON oauth2_p
|
||||
|
||||
CREATE TRIGGER trigger_insert_apikeys BEFORE INSERT ON api_keys FOR EACH ROW EXECUTE FUNCTION insert_apikey_fail_if_user_deleted();
|
||||
|
||||
CREATE TRIGGER trigger_nullify_next_start_at_on_workspace_autostart_modificati AFTER UPDATE ON workspaces FOR EACH ROW EXECUTE FUNCTION nullify_next_start_at_on_workspace_autostart_modification();
|
||||
|
||||
CREATE TRIGGER trigger_update_users AFTER INSERT OR UPDATE ON users FOR EACH ROW WHEN ((new.deleted = true)) EXECUTE FUNCTION delete_deleted_user_resources();
|
||||
|
||||
CREATE TRIGGER trigger_upsert_user_links BEFORE INSERT OR UPDATE ON user_links FOR EACH ROW EXECUTE FUNCTION insert_user_links_fail_if_user_deleted();
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
DROP VIEW workspaces_expanded;
|
||||
|
||||
DROP TRIGGER IF EXISTS trigger_nullify_next_start_at_on_template_autostart_modification ON templates;
|
||||
DROP FUNCTION IF EXISTS nullify_next_start_at_on_template_autostart_modification;
|
||||
|
||||
DROP TRIGGER IF EXISTS trigger_nullify_next_start_at_on_workspace_autostart_modification ON workspaces;
|
||||
DROP FUNCTION IF EXISTS nullify_next_start_at_on_workspace_autostart_modification;
|
||||
|
||||
DROP INDEX workspace_template_id_idx;
|
||||
DROP INDEX workspace_next_start_at_idx;
|
||||
|
||||
ALTER TABLE ONLY workspaces DROP COLUMN IF EXISTS next_start_at;
|
||||
|
||||
CREATE VIEW
|
||||
workspaces_expanded
|
||||
AS
|
||||
SELECT
|
||||
workspaces.*,
|
||||
-- Owner
|
||||
visible_users.avatar_url AS owner_avatar_url,
|
||||
visible_users.username AS owner_username,
|
||||
-- Organization
|
||||
organizations.name AS organization_name,
|
||||
organizations.display_name AS organization_display_name,
|
||||
organizations.icon AS organization_icon,
|
||||
organizations.description AS organization_description,
|
||||
-- Template
|
||||
templates.name AS template_name,
|
||||
templates.display_name AS template_display_name,
|
||||
templates.icon AS template_icon,
|
||||
templates.description AS template_description
|
||||
FROM
|
||||
workspaces
|
||||
INNER JOIN
|
||||
visible_users
|
||||
ON
|
||||
workspaces.owner_id = visible_users.id
|
||||
INNER JOIN
|
||||
organizations
|
||||
ON workspaces.organization_id = organizations.id
|
||||
INNER JOIN
|
||||
templates
|
||||
ON workspaces.template_id = templates.id
|
||||
;
|
||||
|
||||
COMMENT ON VIEW workspaces_expanded IS 'Joins in the display name information such as username, avatar, and organization name.';
|
||||
@@ -0,0 +1,65 @@
|
||||
ALTER TABLE ONLY workspaces ADD COLUMN IF NOT EXISTS next_start_at TIMESTAMPTZ DEFAULT NULL;
|
||||
|
||||
CREATE INDEX workspace_next_start_at_idx ON workspaces USING btree (next_start_at) WHERE (deleted=false);
|
||||
CREATE INDEX workspace_template_id_idx ON workspaces USING btree (template_id) WHERE (deleted=false);
|
||||
|
||||
CREATE FUNCTION nullify_next_start_at_on_workspace_autostart_modification() RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
BEGIN
|
||||
-- A workspace's next_start_at might be invalidated by the following:
|
||||
-- * The autostart schedule has changed independent to next_start_at
|
||||
-- * The workspace has been marked as dormant
|
||||
IF (NEW.autostart_schedule <> OLD.autostart_schedule AND NEW.next_start_at = OLD.next_start_at)
|
||||
OR (NEW.dormant_at IS NOT NULL AND NEW.next_start_at IS NOT NULL)
|
||||
THEN
|
||||
UPDATE workspaces
|
||||
SET next_start_at = NULL
|
||||
WHERE id = NEW.id;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER trigger_nullify_next_start_at_on_workspace_autostart_modification
|
||||
AFTER UPDATE ON workspaces
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE nullify_next_start_at_on_workspace_autostart_modification();
|
||||
|
||||
-- Recreate view
|
||||
DROP VIEW workspaces_expanded;
|
||||
|
||||
CREATE VIEW
|
||||
workspaces_expanded
|
||||
AS
|
||||
SELECT
|
||||
workspaces.*,
|
||||
-- Owner
|
||||
visible_users.avatar_url AS owner_avatar_url,
|
||||
visible_users.username AS owner_username,
|
||||
-- Organization
|
||||
organizations.name AS organization_name,
|
||||
organizations.display_name AS organization_display_name,
|
||||
organizations.icon AS organization_icon,
|
||||
organizations.description AS organization_description,
|
||||
-- Template
|
||||
templates.name AS template_name,
|
||||
templates.display_name AS template_display_name,
|
||||
templates.icon AS template_icon,
|
||||
templates.description AS template_description
|
||||
FROM
|
||||
workspaces
|
||||
INNER JOIN
|
||||
visible_users
|
||||
ON
|
||||
workspaces.owner_id = visible_users.id
|
||||
INNER JOIN
|
||||
organizations
|
||||
ON workspaces.organization_id = organizations.id
|
||||
INNER JOIN
|
||||
templates
|
||||
ON workspaces.template_id = templates.id
|
||||
;
|
||||
|
||||
COMMENT ON VIEW workspaces_expanded IS 'Joins in the display name information such as username, avatar, and organization name.';
|
||||
@@ -214,6 +214,7 @@ func (w Workspace) WorkspaceTable() WorkspaceTable {
|
||||
DeletingAt: w.DeletingAt,
|
||||
AutomaticUpdates: w.AutomaticUpdates,
|
||||
Favorite: w.Favorite,
|
||||
NextStartAt: w.NextStartAt,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -438,6 +439,7 @@ func ConvertWorkspaceRows(rows []GetWorkspacesRow) []Workspace {
|
||||
TemplateDisplayName: r.TemplateDisplayName,
|
||||
TemplateIcon: r.TemplateIcon,
|
||||
TemplateDescription: r.TemplateDescription,
|
||||
NextStartAt: r.NextStartAt,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -290,6 +290,7 @@ func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspa
|
||||
&i.DeletingAt,
|
||||
&i.AutomaticUpdates,
|
||||
&i.Favorite,
|
||||
&i.NextStartAt,
|
||||
&i.OwnerAvatarUrl,
|
||||
&i.OwnerUsername,
|
||||
&i.OrganizationName,
|
||||
|
||||
@@ -2922,6 +2922,7 @@ type Workspace struct {
|
||||
DeletingAt sql.NullTime `db:"deleting_at" json:"deleting_at"`
|
||||
AutomaticUpdates AutomaticUpdates `db:"automatic_updates" json:"automatic_updates"`
|
||||
Favorite bool `db:"favorite" json:"favorite"`
|
||||
NextStartAt sql.NullTime `db:"next_start_at" json:"next_start_at"`
|
||||
OwnerAvatarUrl string `db:"owner_avatar_url" json:"owner_avatar_url"`
|
||||
OwnerUsername string `db:"owner_username" json:"owner_username"`
|
||||
OrganizationName string `db:"organization_name" json:"organization_name"`
|
||||
@@ -3225,5 +3226,6 @@ type WorkspaceTable struct {
|
||||
DeletingAt sql.NullTime `db:"deleting_at" json:"deleting_at"`
|
||||
AutomaticUpdates AutomaticUpdates `db:"automatic_updates" json:"automatic_updates"`
|
||||
// Favorite is true if the workspace owner has favorited the workspace.
|
||||
Favorite bool `db:"favorite" json:"favorite"`
|
||||
Favorite bool `db:"favorite" json:"favorite"`
|
||||
NextStartAt sql.NullTime `db:"next_start_at" json:"next_start_at"`
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ type sqlcQuerier interface {
|
||||
// referenced by the latest build of a workspace.
|
||||
ArchiveUnusedTemplateVersions(ctx context.Context, arg ArchiveUnusedTemplateVersionsParams) ([]uuid.UUID, error)
|
||||
BatchUpdateWorkspaceLastUsedAt(ctx context.Context, arg BatchUpdateWorkspaceLastUsedAtParams) error
|
||||
BatchUpdateWorkspaceNextStartAt(ctx context.Context, arg BatchUpdateWorkspaceNextStartAtParams) error
|
||||
BulkMarkNotificationMessagesFailed(ctx context.Context, arg BulkMarkNotificationMessagesFailedParams) (int64, error)
|
||||
BulkMarkNotificationMessagesSent(ctx context.Context, arg BulkMarkNotificationMessagesSentParams) (int64, error)
|
||||
CleanTailnetCoordinators(ctx context.Context) error
|
||||
@@ -348,6 +349,7 @@ type sqlcQuerier interface {
|
||||
// be used in a WHERE clause.
|
||||
GetWorkspaces(ctx context.Context, arg GetWorkspacesParams) ([]GetWorkspacesRow, error)
|
||||
GetWorkspacesAndAgentsByOwnerID(ctx context.Context, ownerID uuid.UUID) ([]GetWorkspacesAndAgentsByOwnerIDRow, error)
|
||||
GetWorkspacesByTemplateID(ctx context.Context, templateID uuid.UUID) ([]WorkspaceTable, error)
|
||||
GetWorkspacesEligibleForTransition(ctx context.Context, now time.Time) ([]GetWorkspacesEligibleForTransitionRow, error)
|
||||
InsertAPIKey(ctx context.Context, arg InsertAPIKeyParams) (APIKey, error)
|
||||
// We use the organization_id as the id
|
||||
@@ -496,6 +498,7 @@ type sqlcQuerier interface {
|
||||
UpdateWorkspaceDeletedByID(ctx context.Context, arg UpdateWorkspaceDeletedByIDParams) error
|
||||
UpdateWorkspaceDormantDeletingAt(ctx context.Context, arg UpdateWorkspaceDormantDeletingAtParams) (WorkspaceTable, error)
|
||||
UpdateWorkspaceLastUsedAt(ctx context.Context, arg UpdateWorkspaceLastUsedAtParams) error
|
||||
UpdateWorkspaceNextStartAt(ctx context.Context, arg UpdateWorkspaceNextStartAtParams) error
|
||||
// This allows editing the properties of a workspace proxy.
|
||||
UpdateWorkspaceProxy(ctx context.Context, arg UpdateWorkspaceProxyParams) (WorkspaceProxy, error)
|
||||
UpdateWorkspaceProxyDeleted(ctx context.Context, arg UpdateWorkspaceProxyDeletedParams) error
|
||||
|
||||
+137
-17
@@ -11228,7 +11228,7 @@ func (q *sqlQuerier) DeleteOldWorkspaceAgentLogs(ctx context.Context, threshold
|
||||
|
||||
const getWorkspaceAgentAndLatestBuildByAuthToken = `-- name: GetWorkspaceAgentAndLatestBuildByAuthToken :one
|
||||
SELECT
|
||||
workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates, workspaces.favorite,
|
||||
workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates, workspaces.favorite, workspaces.next_start_at,
|
||||
workspace_agents.id, workspace_agents.created_at, workspace_agents.updated_at, workspace_agents.name, workspace_agents.first_connected_at, workspace_agents.last_connected_at, workspace_agents.disconnected_at, workspace_agents.resource_id, workspace_agents.auth_token, workspace_agents.auth_instance_id, workspace_agents.architecture, workspace_agents.environment_variables, workspace_agents.operating_system, workspace_agents.instance_metadata, workspace_agents.resource_metadata, workspace_agents.directory, workspace_agents.version, workspace_agents.last_connected_replica_id, workspace_agents.connection_timeout_seconds, workspace_agents.troubleshooting_url, workspace_agents.motd_file, workspace_agents.lifecycle_state, workspace_agents.expanded_directory, workspace_agents.logs_length, workspace_agents.logs_overflowed, workspace_agents.started_at, workspace_agents.ready_at, workspace_agents.subsystems, workspace_agents.display_apps, workspace_agents.api_version, workspace_agents.display_order,
|
||||
workspace_build_with_user.id, workspace_build_with_user.created_at, workspace_build_with_user.updated_at, workspace_build_with_user.workspace_id, workspace_build_with_user.template_version_id, workspace_build_with_user.build_number, workspace_build_with_user.transition, workspace_build_with_user.initiator_id, workspace_build_with_user.provisioner_state, workspace_build_with_user.job_id, workspace_build_with_user.deadline, workspace_build_with_user.reason, workspace_build_with_user.daily_cost, workspace_build_with_user.max_deadline, workspace_build_with_user.initiator_by_avatar_url, workspace_build_with_user.initiator_by_username
|
||||
FROM
|
||||
@@ -11287,6 +11287,7 @@ func (q *sqlQuerier) GetWorkspaceAgentAndLatestBuildByAuthToken(ctx context.Cont
|
||||
&i.WorkspaceTable.DeletingAt,
|
||||
&i.WorkspaceTable.AutomaticUpdates,
|
||||
&i.WorkspaceTable.Favorite,
|
||||
&i.WorkspaceTable.NextStartAt,
|
||||
&i.WorkspaceAgent.ID,
|
||||
&i.WorkspaceAgent.CreatedAt,
|
||||
&i.WorkspaceAgent.UpdatedAt,
|
||||
@@ -14720,6 +14721,33 @@ func (q *sqlQuerier) BatchUpdateWorkspaceLastUsedAt(ctx context.Context, arg Bat
|
||||
return err
|
||||
}
|
||||
|
||||
const batchUpdateWorkspaceNextStartAt = `-- name: BatchUpdateWorkspaceNextStartAt :exec
|
||||
UPDATE
|
||||
workspaces
|
||||
SET
|
||||
next_start_at = CASE
|
||||
WHEN batch.next_start_at = '0001-01-01 00:00:00+00'::timestamptz THEN NULL
|
||||
ELSE batch.next_start_at
|
||||
END
|
||||
FROM (
|
||||
SELECT
|
||||
unnest($1::uuid[]) AS id,
|
||||
unnest($2::timestamptz[]) AS next_start_at
|
||||
) AS batch
|
||||
WHERE
|
||||
workspaces.id = batch.id
|
||||
`
|
||||
|
||||
type BatchUpdateWorkspaceNextStartAtParams struct {
|
||||
IDs []uuid.UUID `db:"ids" json:"ids"`
|
||||
NextStartAts []time.Time `db:"next_start_ats" json:"next_start_ats"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) BatchUpdateWorkspaceNextStartAt(ctx context.Context, arg BatchUpdateWorkspaceNextStartAtParams) error {
|
||||
_, err := q.db.ExecContext(ctx, batchUpdateWorkspaceNextStartAt, pq.Array(arg.IDs), pq.Array(arg.NextStartAts))
|
||||
return err
|
||||
}
|
||||
|
||||
const favoriteWorkspace = `-- name: FavoriteWorkspace :exec
|
||||
UPDATE workspaces SET favorite = true WHERE id = $1
|
||||
`
|
||||
@@ -14815,7 +14843,7 @@ func (q *sqlQuerier) GetDeploymentWorkspaceStats(ctx context.Context) (GetDeploy
|
||||
|
||||
const getWorkspaceByAgentID = `-- name: GetWorkspaceByAgentID :one
|
||||
SELECT
|
||||
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, owner_avatar_url, owner_username, organization_name, organization_display_name, organization_icon, organization_description, template_name, template_display_name, template_icon, template_description
|
||||
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, owner_avatar_url, owner_username, organization_name, organization_display_name, organization_icon, organization_description, template_name, template_display_name, template_icon, template_description
|
||||
FROM
|
||||
workspaces_expanded as workspaces
|
||||
WHERE
|
||||
@@ -14862,6 +14890,7 @@ func (q *sqlQuerier) GetWorkspaceByAgentID(ctx context.Context, agentID uuid.UUI
|
||||
&i.DeletingAt,
|
||||
&i.AutomaticUpdates,
|
||||
&i.Favorite,
|
||||
&i.NextStartAt,
|
||||
&i.OwnerAvatarUrl,
|
||||
&i.OwnerUsername,
|
||||
&i.OrganizationName,
|
||||
@@ -14878,7 +14907,7 @@ func (q *sqlQuerier) GetWorkspaceByAgentID(ctx context.Context, agentID uuid.UUI
|
||||
|
||||
const getWorkspaceByID = `-- name: GetWorkspaceByID :one
|
||||
SELECT
|
||||
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, owner_avatar_url, owner_username, organization_name, organization_display_name, organization_icon, organization_description, template_name, template_display_name, template_icon, template_description
|
||||
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, owner_avatar_url, owner_username, organization_name, organization_display_name, organization_icon, organization_description, template_name, template_display_name, template_icon, template_description
|
||||
FROM
|
||||
workspaces_expanded
|
||||
WHERE
|
||||
@@ -14906,6 +14935,7 @@ func (q *sqlQuerier) GetWorkspaceByID(ctx context.Context, id uuid.UUID) (Worksp
|
||||
&i.DeletingAt,
|
||||
&i.AutomaticUpdates,
|
||||
&i.Favorite,
|
||||
&i.NextStartAt,
|
||||
&i.OwnerAvatarUrl,
|
||||
&i.OwnerUsername,
|
||||
&i.OrganizationName,
|
||||
@@ -14922,7 +14952,7 @@ func (q *sqlQuerier) GetWorkspaceByID(ctx context.Context, id uuid.UUID) (Worksp
|
||||
|
||||
const getWorkspaceByOwnerIDAndName = `-- name: GetWorkspaceByOwnerIDAndName :one
|
||||
SELECT
|
||||
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, owner_avatar_url, owner_username, organization_name, organization_display_name, organization_icon, organization_description, template_name, template_display_name, template_icon, template_description
|
||||
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, owner_avatar_url, owner_username, organization_name, organization_display_name, organization_icon, organization_description, template_name, template_display_name, template_icon, template_description
|
||||
FROM
|
||||
workspaces_expanded as workspaces
|
||||
WHERE
|
||||
@@ -14957,6 +14987,7 @@ func (q *sqlQuerier) GetWorkspaceByOwnerIDAndName(ctx context.Context, arg GetWo
|
||||
&i.DeletingAt,
|
||||
&i.AutomaticUpdates,
|
||||
&i.Favorite,
|
||||
&i.NextStartAt,
|
||||
&i.OwnerAvatarUrl,
|
||||
&i.OwnerUsername,
|
||||
&i.OrganizationName,
|
||||
@@ -14973,7 +15004,7 @@ func (q *sqlQuerier) GetWorkspaceByOwnerIDAndName(ctx context.Context, arg GetWo
|
||||
|
||||
const getWorkspaceByWorkspaceAppID = `-- name: GetWorkspaceByWorkspaceAppID :one
|
||||
SELECT
|
||||
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, owner_avatar_url, owner_username, organization_name, organization_display_name, organization_icon, organization_description, template_name, template_display_name, template_icon, template_description
|
||||
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, owner_avatar_url, owner_username, organization_name, organization_display_name, organization_icon, organization_description, template_name, template_display_name, template_icon, template_description
|
||||
FROM
|
||||
workspaces_expanded as workspaces
|
||||
WHERE
|
||||
@@ -15027,6 +15058,7 @@ func (q *sqlQuerier) GetWorkspaceByWorkspaceAppID(ctx context.Context, workspace
|
||||
&i.DeletingAt,
|
||||
&i.AutomaticUpdates,
|
||||
&i.Favorite,
|
||||
&i.NextStartAt,
|
||||
&i.OwnerAvatarUrl,
|
||||
&i.OwnerUsername,
|
||||
&i.OrganizationName,
|
||||
@@ -15088,7 +15120,7 @@ SELECT
|
||||
),
|
||||
filtered_workspaces AS (
|
||||
SELECT
|
||||
workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates, workspaces.favorite, workspaces.owner_avatar_url, workspaces.owner_username, workspaces.organization_name, workspaces.organization_display_name, workspaces.organization_icon, workspaces.organization_description, workspaces.template_name, workspaces.template_display_name, workspaces.template_icon, workspaces.template_description,
|
||||
workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates, workspaces.favorite, workspaces.next_start_at, workspaces.owner_avatar_url, workspaces.owner_username, workspaces.organization_name, workspaces.organization_display_name, workspaces.organization_icon, workspaces.organization_description, workspaces.template_name, workspaces.template_display_name, workspaces.template_icon, workspaces.template_description,
|
||||
latest_build.template_version_id,
|
||||
latest_build.template_version_name,
|
||||
latest_build.completed_at as latest_build_completed_at,
|
||||
@@ -15328,7 +15360,7 @@ WHERE
|
||||
-- @authorize_filter
|
||||
), filtered_workspaces_order AS (
|
||||
SELECT
|
||||
fw.id, fw.created_at, fw.updated_at, fw.owner_id, fw.organization_id, fw.template_id, fw.deleted, fw.name, fw.autostart_schedule, fw.ttl, fw.last_used_at, fw.dormant_at, fw.deleting_at, fw.automatic_updates, fw.favorite, fw.owner_avatar_url, fw.owner_username, fw.organization_name, fw.organization_display_name, fw.organization_icon, fw.organization_description, fw.template_name, fw.template_display_name, fw.template_icon, fw.template_description, fw.template_version_id, fw.template_version_name, fw.latest_build_completed_at, fw.latest_build_canceled_at, fw.latest_build_error, fw.latest_build_transition, fw.latest_build_status
|
||||
fw.id, fw.created_at, fw.updated_at, fw.owner_id, fw.organization_id, fw.template_id, fw.deleted, fw.name, fw.autostart_schedule, fw.ttl, fw.last_used_at, fw.dormant_at, fw.deleting_at, fw.automatic_updates, fw.favorite, fw.next_start_at, fw.owner_avatar_url, fw.owner_username, fw.organization_name, fw.organization_display_name, fw.organization_icon, fw.organization_description, fw.template_name, fw.template_display_name, fw.template_icon, fw.template_description, fw.template_version_id, fw.template_version_name, fw.latest_build_completed_at, fw.latest_build_canceled_at, fw.latest_build_error, fw.latest_build_transition, fw.latest_build_status
|
||||
FROM
|
||||
filtered_workspaces fw
|
||||
ORDER BY
|
||||
@@ -15349,7 +15381,7 @@ WHERE
|
||||
$20
|
||||
), filtered_workspaces_order_with_summary AS (
|
||||
SELECT
|
||||
fwo.id, fwo.created_at, fwo.updated_at, fwo.owner_id, fwo.organization_id, fwo.template_id, fwo.deleted, fwo.name, fwo.autostart_schedule, fwo.ttl, fwo.last_used_at, fwo.dormant_at, fwo.deleting_at, fwo.automatic_updates, fwo.favorite, fwo.owner_avatar_url, fwo.owner_username, fwo.organization_name, fwo.organization_display_name, fwo.organization_icon, fwo.organization_description, fwo.template_name, fwo.template_display_name, fwo.template_icon, fwo.template_description, fwo.template_version_id, fwo.template_version_name, fwo.latest_build_completed_at, fwo.latest_build_canceled_at, fwo.latest_build_error, fwo.latest_build_transition, fwo.latest_build_status
|
||||
fwo.id, fwo.created_at, fwo.updated_at, fwo.owner_id, fwo.organization_id, fwo.template_id, fwo.deleted, fwo.name, fwo.autostart_schedule, fwo.ttl, fwo.last_used_at, fwo.dormant_at, fwo.deleting_at, fwo.automatic_updates, fwo.favorite, fwo.next_start_at, fwo.owner_avatar_url, fwo.owner_username, fwo.organization_name, fwo.organization_display_name, fwo.organization_icon, fwo.organization_description, fwo.template_name, fwo.template_display_name, fwo.template_icon, fwo.template_description, fwo.template_version_id, fwo.template_version_name, fwo.latest_build_completed_at, fwo.latest_build_canceled_at, fwo.latest_build_error, fwo.latest_build_transition, fwo.latest_build_status
|
||||
FROM
|
||||
filtered_workspaces_order fwo
|
||||
-- Return a technical summary row with total count of workspaces.
|
||||
@@ -15371,6 +15403,7 @@ WHERE
|
||||
'0001-01-01 00:00:00+00'::timestamptz, -- deleting_at
|
||||
'never'::automatic_updates, -- automatic_updates
|
||||
false, -- favorite
|
||||
'0001-01-01 00:00:00+00'::timestamptz, -- next_start_at
|
||||
'', -- owner_avatar_url
|
||||
'', -- owner_username
|
||||
'', -- organization_name
|
||||
@@ -15398,7 +15431,7 @@ WHERE
|
||||
filtered_workspaces
|
||||
)
|
||||
SELECT
|
||||
fwos.id, fwos.created_at, fwos.updated_at, fwos.owner_id, fwos.organization_id, fwos.template_id, fwos.deleted, fwos.name, fwos.autostart_schedule, fwos.ttl, fwos.last_used_at, fwos.dormant_at, fwos.deleting_at, fwos.automatic_updates, fwos.favorite, fwos.owner_avatar_url, fwos.owner_username, fwos.organization_name, fwos.organization_display_name, fwos.organization_icon, fwos.organization_description, fwos.template_name, fwos.template_display_name, fwos.template_icon, fwos.template_description, fwos.template_version_id, fwos.template_version_name, fwos.latest_build_completed_at, fwos.latest_build_canceled_at, fwos.latest_build_error, fwos.latest_build_transition, fwos.latest_build_status,
|
||||
fwos.id, fwos.created_at, fwos.updated_at, fwos.owner_id, fwos.organization_id, fwos.template_id, fwos.deleted, fwos.name, fwos.autostart_schedule, fwos.ttl, fwos.last_used_at, fwos.dormant_at, fwos.deleting_at, fwos.automatic_updates, fwos.favorite, fwos.next_start_at, fwos.owner_avatar_url, fwos.owner_username, fwos.organization_name, fwos.organization_display_name, fwos.organization_icon, fwos.organization_description, fwos.template_name, fwos.template_display_name, fwos.template_icon, fwos.template_description, fwos.template_version_id, fwos.template_version_name, fwos.latest_build_completed_at, fwos.latest_build_canceled_at, fwos.latest_build_error, fwos.latest_build_transition, fwos.latest_build_status,
|
||||
tc.count
|
||||
FROM
|
||||
filtered_workspaces_order_with_summary fwos
|
||||
@@ -15447,6 +15480,7 @@ type GetWorkspacesRow struct {
|
||||
DeletingAt sql.NullTime `db:"deleting_at" json:"deleting_at"`
|
||||
AutomaticUpdates AutomaticUpdates `db:"automatic_updates" json:"automatic_updates"`
|
||||
Favorite bool `db:"favorite" json:"favorite"`
|
||||
NextStartAt sql.NullTime `db:"next_start_at" json:"next_start_at"`
|
||||
OwnerAvatarUrl string `db:"owner_avatar_url" json:"owner_avatar_url"`
|
||||
OwnerUsername string `db:"owner_username" json:"owner_username"`
|
||||
OrganizationName string `db:"organization_name" json:"organization_name"`
|
||||
@@ -15518,6 +15552,7 @@ func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams)
|
||||
&i.DeletingAt,
|
||||
&i.AutomaticUpdates,
|
||||
&i.Favorite,
|
||||
&i.NextStartAt,
|
||||
&i.OwnerAvatarUrl,
|
||||
&i.OwnerUsername,
|
||||
&i.OrganizationName,
|
||||
@@ -15625,6 +15660,50 @@ func (q *sqlQuerier) GetWorkspacesAndAgentsByOwnerID(ctx context.Context, ownerI
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getWorkspacesByTemplateID = `-- name: GetWorkspacesByTemplateID :many
|
||||
SELECT id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at FROM workspaces WHERE template_id = $1 AND deleted = false
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetWorkspacesByTemplateID(ctx context.Context, templateID uuid.UUID) ([]WorkspaceTable, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getWorkspacesByTemplateID, templateID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []WorkspaceTable
|
||||
for rows.Next() {
|
||||
var i WorkspaceTable
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.OwnerID,
|
||||
&i.OrganizationID,
|
||||
&i.TemplateID,
|
||||
&i.Deleted,
|
||||
&i.Name,
|
||||
&i.AutostartSchedule,
|
||||
&i.Ttl,
|
||||
&i.LastUsedAt,
|
||||
&i.DormantAt,
|
||||
&i.DeletingAt,
|
||||
&i.AutomaticUpdates,
|
||||
&i.Favorite,
|
||||
&i.NextStartAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getWorkspacesEligibleForTransition = `-- name: GetWorkspacesEligibleForTransition :many
|
||||
SELECT
|
||||
workspaces.id,
|
||||
@@ -15670,12 +15749,25 @@ WHERE
|
||||
-- * The workspace's owner is active.
|
||||
-- * The provisioner job did not fail.
|
||||
-- * The workspace build was a stop transition.
|
||||
-- * The workspace is not dormant
|
||||
-- * The workspace has an autostart schedule.
|
||||
-- * It is after the workspace's next start time.
|
||||
(
|
||||
users.status = 'active'::user_status AND
|
||||
provisioner_jobs.job_status != 'failed'::provisioner_job_status AND
|
||||
workspace_builds.transition = 'stop'::workspace_transition AND
|
||||
workspaces.autostart_schedule IS NOT NULL
|
||||
workspaces.dormant_at IS NULL AND
|
||||
workspaces.autostart_schedule IS NOT NULL AND
|
||||
(
|
||||
-- next_start_at might be null in these two scenarios:
|
||||
-- * A coder instance was updated and we haven't updated next_start_at yet.
|
||||
-- * A database trigger made it null because of an update to a related column.
|
||||
--
|
||||
-- When this occurs, we return the workspace so the Coder server can
|
||||
-- compute a valid next start at and update it.
|
||||
workspaces.next_start_at IS NULL OR
|
||||
workspaces.next_start_at <= $1 :: timestamptz
|
||||
)
|
||||
) OR
|
||||
|
||||
-- A workspace may be eligible for dormant stop if the following are true:
|
||||
@@ -15774,10 +15866,11 @@ INSERT INTO
|
||||
autostart_schedule,
|
||||
ttl,
|
||||
last_used_at,
|
||||
automatic_updates
|
||||
automatic_updates,
|
||||
next_start_at
|
||||
)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at
|
||||
`
|
||||
|
||||
type InsertWorkspaceParams struct {
|
||||
@@ -15792,6 +15885,7 @@ type InsertWorkspaceParams struct {
|
||||
Ttl sql.NullInt64 `db:"ttl" json:"ttl"`
|
||||
LastUsedAt time.Time `db:"last_used_at" json:"last_used_at"`
|
||||
AutomaticUpdates AutomaticUpdates `db:"automatic_updates" json:"automatic_updates"`
|
||||
NextStartAt sql.NullTime `db:"next_start_at" json:"next_start_at"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) InsertWorkspace(ctx context.Context, arg InsertWorkspaceParams) (WorkspaceTable, error) {
|
||||
@@ -15807,6 +15901,7 @@ func (q *sqlQuerier) InsertWorkspace(ctx context.Context, arg InsertWorkspacePar
|
||||
arg.Ttl,
|
||||
arg.LastUsedAt,
|
||||
arg.AutomaticUpdates,
|
||||
arg.NextStartAt,
|
||||
)
|
||||
var i WorkspaceTable
|
||||
err := row.Scan(
|
||||
@@ -15825,6 +15920,7 @@ func (q *sqlQuerier) InsertWorkspace(ctx context.Context, arg InsertWorkspacePar
|
||||
&i.DeletingAt,
|
||||
&i.AutomaticUpdates,
|
||||
&i.Favorite,
|
||||
&i.NextStartAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -15864,7 +15960,7 @@ SET
|
||||
WHERE
|
||||
id = $1
|
||||
AND deleted = false
|
||||
RETURNING id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite
|
||||
RETURNING id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at
|
||||
`
|
||||
|
||||
type UpdateWorkspaceParams struct {
|
||||
@@ -15891,6 +15987,7 @@ func (q *sqlQuerier) UpdateWorkspace(ctx context.Context, arg UpdateWorkspacePar
|
||||
&i.DeletingAt,
|
||||
&i.AutomaticUpdates,
|
||||
&i.Favorite,
|
||||
&i.NextStartAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -15918,7 +16015,8 @@ const updateWorkspaceAutostart = `-- name: UpdateWorkspaceAutostart :exec
|
||||
UPDATE
|
||||
workspaces
|
||||
SET
|
||||
autostart_schedule = $2
|
||||
autostart_schedule = $2,
|
||||
next_start_at = $3
|
||||
WHERE
|
||||
id = $1
|
||||
`
|
||||
@@ -15926,10 +16024,11 @@ WHERE
|
||||
type UpdateWorkspaceAutostartParams struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
AutostartSchedule sql.NullString `db:"autostart_schedule" json:"autostart_schedule"`
|
||||
NextStartAt sql.NullTime `db:"next_start_at" json:"next_start_at"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) UpdateWorkspaceAutostart(ctx context.Context, arg UpdateWorkspaceAutostartParams) error {
|
||||
_, err := q.db.ExecContext(ctx, updateWorkspaceAutostart, arg.ID, arg.AutostartSchedule)
|
||||
_, err := q.db.ExecContext(ctx, updateWorkspaceAutostart, arg.ID, arg.AutostartSchedule, arg.NextStartAt)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -15977,7 +16076,7 @@ WHERE
|
||||
workspaces.id = $1
|
||||
AND templates.id = workspaces.template_id
|
||||
RETURNING
|
||||
workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates, workspaces.favorite
|
||||
workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates, workspaces.favorite, workspaces.next_start_at
|
||||
`
|
||||
|
||||
type UpdateWorkspaceDormantDeletingAtParams struct {
|
||||
@@ -16004,6 +16103,7 @@ func (q *sqlQuerier) UpdateWorkspaceDormantDeletingAt(ctx context.Context, arg U
|
||||
&i.DeletingAt,
|
||||
&i.AutomaticUpdates,
|
||||
&i.Favorite,
|
||||
&i.NextStartAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -16027,6 +16127,25 @@ func (q *sqlQuerier) UpdateWorkspaceLastUsedAt(ctx context.Context, arg UpdateWo
|
||||
return err
|
||||
}
|
||||
|
||||
const updateWorkspaceNextStartAt = `-- name: UpdateWorkspaceNextStartAt :exec
|
||||
UPDATE
|
||||
workspaces
|
||||
SET
|
||||
next_start_at = $2
|
||||
WHERE
|
||||
id = $1
|
||||
`
|
||||
|
||||
type UpdateWorkspaceNextStartAtParams struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
NextStartAt sql.NullTime `db:"next_start_at" json:"next_start_at"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) UpdateWorkspaceNextStartAt(ctx context.Context, arg UpdateWorkspaceNextStartAtParams) error {
|
||||
_, err := q.db.ExecContext(ctx, updateWorkspaceNextStartAt, arg.ID, arg.NextStartAt)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateWorkspaceTTL = `-- name: UpdateWorkspaceTTL :exec
|
||||
UPDATE
|
||||
workspaces
|
||||
@@ -16059,7 +16178,7 @@ WHERE
|
||||
template_id = $3
|
||||
AND
|
||||
dormant_at IS NOT NULL
|
||||
RETURNING id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite
|
||||
RETURNING id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at
|
||||
`
|
||||
|
||||
type UpdateWorkspacesDormantDeletingAtByTemplateIDParams struct {
|
||||
@@ -16093,6 +16212,7 @@ func (q *sqlQuerier) UpdateWorkspacesDormantDeletingAtByTemplateID(ctx context.C
|
||||
&i.DeletingAt,
|
||||
&i.AutomaticUpdates,
|
||||
&i.Favorite,
|
||||
&i.NextStartAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -368,6 +368,7 @@ WHERE
|
||||
'0001-01-01 00:00:00+00'::timestamptz, -- deleting_at
|
||||
'never'::automatic_updates, -- automatic_updates
|
||||
false, -- favorite
|
||||
'0001-01-01 00:00:00+00'::timestamptz, -- next_start_at
|
||||
'', -- owner_avatar_url
|
||||
'', -- owner_username
|
||||
'', -- organization_name
|
||||
@@ -435,10 +436,11 @@ INSERT INTO
|
||||
autostart_schedule,
|
||||
ttl,
|
||||
last_used_at,
|
||||
automatic_updates
|
||||
automatic_updates,
|
||||
next_start_at
|
||||
)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING *;
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING *;
|
||||
|
||||
-- name: UpdateWorkspaceDeletedByID :exec
|
||||
UPDATE
|
||||
@@ -462,10 +464,35 @@ RETURNING *;
|
||||
UPDATE
|
||||
workspaces
|
||||
SET
|
||||
autostart_schedule = $2
|
||||
autostart_schedule = $2,
|
||||
next_start_at = $3
|
||||
WHERE
|
||||
id = $1;
|
||||
|
||||
-- name: UpdateWorkspaceNextStartAt :exec
|
||||
UPDATE
|
||||
workspaces
|
||||
SET
|
||||
next_start_at = $2
|
||||
WHERE
|
||||
id = $1;
|
||||
|
||||
-- name: BatchUpdateWorkspaceNextStartAt :exec
|
||||
UPDATE
|
||||
workspaces
|
||||
SET
|
||||
next_start_at = CASE
|
||||
WHEN batch.next_start_at = '0001-01-01 00:00:00+00'::timestamptz THEN NULL
|
||||
ELSE batch.next_start_at
|
||||
END
|
||||
FROM (
|
||||
SELECT
|
||||
unnest(sqlc.arg(ids)::uuid[]) AS id,
|
||||
unnest(sqlc.arg(next_start_ats)::timestamptz[]) AS next_start_at
|
||||
) AS batch
|
||||
WHERE
|
||||
workspaces.id = batch.id;
|
||||
|
||||
-- name: UpdateWorkspaceTTL :exec
|
||||
UPDATE
|
||||
workspaces
|
||||
@@ -600,12 +627,25 @@ WHERE
|
||||
-- * The workspace's owner is active.
|
||||
-- * The provisioner job did not fail.
|
||||
-- * The workspace build was a stop transition.
|
||||
-- * The workspace is not dormant
|
||||
-- * The workspace has an autostart schedule.
|
||||
-- * It is after the workspace's next start time.
|
||||
(
|
||||
users.status = 'active'::user_status AND
|
||||
provisioner_jobs.job_status != 'failed'::provisioner_job_status AND
|
||||
workspace_builds.transition = 'stop'::workspace_transition AND
|
||||
workspaces.autostart_schedule IS NOT NULL
|
||||
workspaces.dormant_at IS NULL AND
|
||||
workspaces.autostart_schedule IS NOT NULL AND
|
||||
(
|
||||
-- next_start_at might be null in these two scenarios:
|
||||
-- * A coder instance was updated and we haven't updated next_start_at yet.
|
||||
-- * A database trigger made it null because of an update to a related column.
|
||||
--
|
||||
-- When this occurs, we return the workspace so the Coder server can
|
||||
-- compute a valid next start at and update it.
|
||||
workspaces.next_start_at IS NULL OR
|
||||
workspaces.next_start_at <= @now :: timestamptz
|
||||
)
|
||||
) OR
|
||||
|
||||
-- A workspace may be eligible for dormant stop if the following are true:
|
||||
@@ -761,3 +801,6 @@ WHERE
|
||||
-- Authorize Filter clause will be injected below in GetAuthorizedWorkspacesAndAgentsByOwnerID
|
||||
-- @authorize_filter
|
||||
GROUP BY workspaces.id, workspaces.name, latest_build.job_status, latest_build.job_id, latest_build.transition;
|
||||
|
||||
-- name: GetWorkspacesByTemplateID :many
|
||||
SELECT * FROM workspaces WHERE template_id = $1 AND deleted = false;
|
||||
|
||||
@@ -1438,9 +1438,11 @@ func (s *server) CompleteJob(ctx context.Context, completed *proto.CompletedJob)
|
||||
return getWorkspaceError
|
||||
}
|
||||
|
||||
templateScheduleStore := *s.TemplateScheduleStore.Load()
|
||||
|
||||
autoStop, err := schedule.CalculateAutostop(ctx, schedule.CalculateAutostopParams{
|
||||
Database: db,
|
||||
TemplateScheduleStore: *s.TemplateScheduleStore.Load(),
|
||||
TemplateScheduleStore: templateScheduleStore,
|
||||
UserQuietHoursScheduleStore: *s.UserQuietHoursScheduleStore.Load(),
|
||||
Now: now,
|
||||
Workspace: workspace.WorkspaceTable(),
|
||||
@@ -1451,6 +1453,24 @@ func (s *server) CompleteJob(ctx context.Context, completed *proto.CompletedJob)
|
||||
return xerrors.Errorf("calculate auto stop: %w", err)
|
||||
}
|
||||
|
||||
if workspace.AutostartSchedule.Valid {
|
||||
templateScheduleOptions, err := templateScheduleStore.Get(ctx, db, workspace.TemplateID)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get template schedule options: %w", err)
|
||||
}
|
||||
|
||||
nextStartAt, err := schedule.NextAllowedAutostart(now, workspace.AutostartSchedule.String, templateScheduleOptions)
|
||||
if err == nil {
|
||||
err = db.UpdateWorkspaceNextStartAt(ctx, database.UpdateWorkspaceNextStartAtParams{
|
||||
ID: workspace.ID,
|
||||
NextStartAt: sql.NullTime{Valid: true, Time: nextStartAt.UTC()},
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("update workspace next start at: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = db.UpdateProvisionerJobWithCompleteByID(ctx, database.UpdateProvisionerJobWithCompleteByIDParams{
|
||||
ID: jobID,
|
||||
UpdatedAt: now,
|
||||
|
||||
@@ -3,9 +3,13 @@ package schedule
|
||||
import (
|
||||
"time"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/schedule/cron"
|
||||
)
|
||||
|
||||
var ErrNoAllowedAutostart = xerrors.New("no allowed autostart")
|
||||
|
||||
// NextAutostart takes the workspace and template schedule and returns the next autostart schedule
|
||||
// after "at". The boolean returned is if the autostart should be allowed to start based on the template
|
||||
// schedule.
|
||||
@@ -28,3 +32,19 @@ func NextAutostart(at time.Time, wsSchedule string, templateSchedule TemplateSch
|
||||
|
||||
return zonedTransition, allowed
|
||||
}
|
||||
|
||||
func NextAllowedAutostart(at time.Time, wsSchedule string, templateSchedule TemplateScheduleOptions) (time.Time, error) {
|
||||
next := at
|
||||
|
||||
// Our cron schedules work on a weekly basis, so to ensure we've exhausted all
|
||||
// possible autostart times we need to check up to 7 days worth of autostarts.
|
||||
for next.Sub(at) < 7*24*time.Hour {
|
||||
var valid bool
|
||||
next, valid = NextAutostart(next, wsSchedule, templateSchedule)
|
||||
if valid {
|
||||
return next, nil
|
||||
}
|
||||
}
|
||||
|
||||
return time.Time{}, ErrNoAllowedAutostart
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package schedule_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/schedule"
|
||||
)
|
||||
|
||||
func TestNextAllowedAutostart(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("WhenScheduleOutOfSync", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// 1st January 2024 is a Monday
|
||||
at := time.Date(2024, time.January, 1, 10, 0, 0, 0, time.UTC)
|
||||
// Monday-Friday 9:00AM UTC
|
||||
sched := "CRON_TZ=UTC 00 09 * * 1-5"
|
||||
// Only allow an autostart on mondays
|
||||
opts := schedule.TemplateScheduleOptions{
|
||||
AutostartRequirement: schedule.TemplateAutostartRequirement{
|
||||
DaysOfWeek: 0b00000001,
|
||||
},
|
||||
}
|
||||
|
||||
// NextAutostart will return a non-allowed autostart time as
|
||||
// our AutostartRequirement only allows Mondays but we expect
|
||||
// this to return a Tuesday.
|
||||
next, allowed := schedule.NextAutostart(at, sched, opts)
|
||||
require.False(t, allowed)
|
||||
require.Equal(t, time.Date(2024, time.January, 2, 9, 0, 0, 0, time.UTC), next)
|
||||
|
||||
// NextAllowedAutostart should return the next allowed autostart time.
|
||||
next, err := schedule.NextAllowedAutostart(at, sched, opts)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, time.Date(2024, time.January, 8, 9, 0, 0, 0, time.UTC), next)
|
||||
})
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/provisionerdserver"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
"github.com/coder/coder/v2/coderd/rbac/policy"
|
||||
"github.com/coder/coder/v2/coderd/schedule"
|
||||
"github.com/coder/coder/v2/coderd/schedule/cron"
|
||||
"github.com/coder/coder/v2/coderd/searchquery"
|
||||
"github.com/coder/coder/v2/coderd/telemetry"
|
||||
@@ -555,6 +556,14 @@ func createWorkspace(
|
||||
return
|
||||
}
|
||||
|
||||
nextStartAt := sql.NullTime{}
|
||||
if dbAutostartSchedule.Valid {
|
||||
next, err := schedule.NextAllowedAutostart(dbtime.Now(), dbAutostartSchedule.String, templateSchedule)
|
||||
if err == nil {
|
||||
nextStartAt = sql.NullTime{Valid: true, Time: dbtime.Time(next.UTC())}
|
||||
}
|
||||
}
|
||||
|
||||
dbTTL, err := validWorkspaceTTLMillis(req.TTLMillis, templateSchedule.DefaultTTL)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
@@ -620,6 +629,7 @@ func createWorkspace(
|
||||
TemplateID: template.ID,
|
||||
Name: req.Name,
|
||||
AutostartSchedule: dbAutostartSchedule,
|
||||
NextStartAt: nextStartAt,
|
||||
Ttl: dbTTL,
|
||||
// The workspaces page will sort by last used at, and it's useful to
|
||||
// have the newly created workspace at the top of the list!
|
||||
@@ -881,9 +891,18 @@ func (api *API) putWorkspaceAutostart(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
nextStartAt := sql.NullTime{}
|
||||
if dbSched.Valid {
|
||||
next, err := schedule.NextAllowedAutostart(dbtime.Now(), dbSched.String, templateSchedule)
|
||||
if err == nil {
|
||||
nextStartAt = sql.NullTime{Valid: true, Time: dbtime.Time(next.UTC())}
|
||||
}
|
||||
}
|
||||
|
||||
err = api.Database.UpdateWorkspaceAutostart(ctx, database.UpdateWorkspaceAutostartParams{
|
||||
ID: workspace.ID,
|
||||
AutostartSchedule: dbSched,
|
||||
NextStartAt: nextStartAt,
|
||||
})
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
@@ -1903,6 +1922,11 @@ func convertWorkspace(
|
||||
deletingAt = &workspace.DeletingAt.Time
|
||||
}
|
||||
|
||||
var nextStartAt *time.Time
|
||||
if workspace.NextStartAt.Valid {
|
||||
nextStartAt = &workspace.NextStartAt.Time
|
||||
}
|
||||
|
||||
failingAgents := []uuid.UUID{}
|
||||
for _, resource := range workspaceBuild.Resources {
|
||||
for _, agent := range resource.Agents {
|
||||
@@ -1953,6 +1977,7 @@ func convertWorkspace(
|
||||
AutomaticUpdates: codersdk.AutomaticUpdates(workspace.AutomaticUpdates),
|
||||
AllowRenames: allowRenames,
|
||||
Favorite: requesterFavorite,
|
||||
NextStartAt: nextStartAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -63,6 +63,7 @@ type Workspace struct {
|
||||
AutomaticUpdates AutomaticUpdates `json:"automatic_updates" enums:"always,never"`
|
||||
AllowRenames bool `json:"allow_renames"`
|
||||
Favorite bool `json:"favorite"`
|
||||
NextStartAt *time.Time `json:"next_start_at" format:"date-time"`
|
||||
}
|
||||
|
||||
func (w Workspace) FullName() string {
|
||||
|
||||
@@ -28,7 +28,7 @@ We track the following resources:
|
||||
| User<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>avatar_url</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>true</td></tr><tr><td>email</td><td>true</td></tr><tr><td>github_com_user_id</td><td>false</td></tr><tr><td>hashed_one_time_passcode</td><td>false</td></tr><tr><td>hashed_password</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_seen_at</td><td>false</td></tr><tr><td>login_type</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>one_time_passcode_expires_at</td><td>true</td></tr><tr><td>quiet_hours_schedule</td><td>true</td></tr><tr><td>rbac_roles</td><td>true</td></tr><tr><td>status</td><td>true</td></tr><tr><td>theme_preference</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>username</td><td>true</td></tr></tbody></table> |
|
||||
| WorkspaceBuild<br><i>start, stop</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>build_number</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>daily_cost</td><td>false</td></tr><tr><td>deadline</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>initiator_by_avatar_url</td><td>false</td></tr><tr><td>initiator_by_username</td><td>false</td></tr><tr><td>initiator_id</td><td>false</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>max_deadline</td><td>false</td></tr><tr><td>provisioner_state</td><td>false</td></tr><tr><td>reason</td><td>false</td></tr><tr><td>template_version_id</td><td>true</td></tr><tr><td>transition</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>workspace_id</td><td>false</td></tr></tbody></table> |
|
||||
| WorkspaceProxy<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>true</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>derp_enabled</td><td>true</td></tr><tr><td>derp_only</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>region_id</td><td>true</td></tr><tr><td>token_hashed_secret</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>url</td><td>true</td></tr><tr><td>version</td><td>true</td></tr><tr><td>wildcard_hostname</td><td>true</td></tr></tbody></table> |
|
||||
| WorkspaceTable<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>automatic_updates</td><td>true</td></tr><tr><td>autostart_schedule</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>deleting_at</td><td>true</td></tr><tr><td>dormant_at</td><td>true</td></tr><tr><td>favorite</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_used_at</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>owner_id</td><td>true</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>ttl</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
|
||||
| WorkspaceTable<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>automatic_updates</td><td>true</td></tr><tr><td>autostart_schedule</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>deleting_at</td><td>true</td></tr><tr><td>dormant_at</td><td>true</td></tr><tr><td>favorite</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_used_at</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>next_start_at</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>owner_id</td><td>true</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>ttl</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
|
||||
|
||||
<!-- End generated by 'make docs/admin/security/audit-logs.md'. -->
|
||||
|
||||
|
||||
Generated
+3
@@ -6733,6 +6733,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
|
||||
"workspace_owner_name": "string"
|
||||
},
|
||||
"name": "string",
|
||||
"next_start_at": "2019-08-24T14:15:22Z",
|
||||
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||
"organization_name": "string",
|
||||
"outdated": true,
|
||||
@@ -6767,6 +6768,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
|
||||
| `last_used_at` | string | false | | |
|
||||
| `latest_build` | [codersdk.WorkspaceBuild](#codersdkworkspacebuild) | false | | |
|
||||
| `name` | string | false | | |
|
||||
| `next_start_at` | string | false | | |
|
||||
| `organization_id` | string | false | | |
|
||||
| `organization_name` | string | false | | |
|
||||
| `outdated` | boolean | false | | |
|
||||
@@ -8064,6 +8066,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
|
||||
"workspace_owner_name": "string"
|
||||
},
|
||||
"name": "string",
|
||||
"next_start_at": "2019-08-24T14:15:22Z",
|
||||
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||
"organization_name": "string",
|
||||
"outdated": true,
|
||||
|
||||
Generated
+6
@@ -222,6 +222,7 @@ of the template will be used.
|
||||
"workspace_owner_name": "string"
|
||||
},
|
||||
"name": "string",
|
||||
"next_start_at": "2019-08-24T14:15:22Z",
|
||||
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||
"organization_name": "string",
|
||||
"outdated": true,
|
||||
@@ -445,6 +446,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam
|
||||
"workspace_owner_name": "string"
|
||||
},
|
||||
"name": "string",
|
||||
"next_start_at": "2019-08-24T14:15:22Z",
|
||||
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||
"organization_name": "string",
|
||||
"outdated": true,
|
||||
@@ -692,6 +694,7 @@ of the template will be used.
|
||||
"workspace_owner_name": "string"
|
||||
},
|
||||
"name": "string",
|
||||
"next_start_at": "2019-08-24T14:15:22Z",
|
||||
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||
"organization_name": "string",
|
||||
"outdated": true,
|
||||
@@ -914,6 +917,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces \
|
||||
"workspace_owner_name": "string"
|
||||
},
|
||||
"name": "string",
|
||||
"next_start_at": "2019-08-24T14:15:22Z",
|
||||
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||
"organization_name": "string",
|
||||
"outdated": true,
|
||||
@@ -1138,6 +1142,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace} \
|
||||
"workspace_owner_name": "string"
|
||||
},
|
||||
"name": "string",
|
||||
"next_start_at": "2019-08-24T14:15:22Z",
|
||||
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||
"organization_name": "string",
|
||||
"outdated": true,
|
||||
@@ -1477,6 +1482,7 @@ curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/dormant \
|
||||
"workspace_owner_name": "string"
|
||||
},
|
||||
"name": "string",
|
||||
"next_start_at": "2019-08-24T14:15:22Z",
|
||||
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||
"organization_name": "string",
|
||||
"outdated": true,
|
||||
|
||||
@@ -165,6 +165,7 @@ var auditableResourcesTypes = map[any]map[string]Action{
|
||||
"deleting_at": ActionTrack,
|
||||
"automatic_updates": ActionTrack,
|
||||
"favorite": ActionTrack,
|
||||
"next_start_at": ActionTrack,
|
||||
},
|
||||
&database.WorkspaceBuild{}: {
|
||||
"id": ActionIgnore,
|
||||
|
||||
@@ -738,7 +738,7 @@ func (api *API) updateEntitlements(ctx context.Context) error {
|
||||
|
||||
if initial, changed, enabled := featureChanged(codersdk.FeatureAdvancedTemplateScheduling); shouldUpdate(initial, changed, enabled) {
|
||||
if enabled {
|
||||
templateStore := schedule.NewEnterpriseTemplateScheduleStore(api.AGPL.UserQuietHoursScheduleStore, api.NotificationsEnqueuer, api.Logger.Named("template.schedule-store"))
|
||||
templateStore := schedule.NewEnterpriseTemplateScheduleStore(api.AGPL.UserQuietHoursScheduleStore, api.NotificationsEnqueuer, api.Logger.Named("template.schedule-store"), api.Clock)
|
||||
templateStoreInterface := agplschedule.TemplateScheduleStore(templateStore)
|
||||
api.AGPL.TemplateScheduleStore.Store(&templateStoreInterface)
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
agpl "github.com/coder/coder/v2/coderd/schedule"
|
||||
"github.com/coder/coder/v2/coderd/tracing"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/quartz"
|
||||
)
|
||||
|
||||
// EnterpriseTemplateScheduleStore provides an agpl.TemplateScheduleStore that
|
||||
@@ -30,8 +31,8 @@ type EnterpriseTemplateScheduleStore struct {
|
||||
// update.
|
||||
UserQuietHoursScheduleStore *atomic.Pointer[agpl.UserQuietHoursScheduleStore]
|
||||
|
||||
// Custom time.Now() function to use in tests. Defaults to dbtime.Now().
|
||||
TimeNowFn func() time.Time
|
||||
// Clock for testing
|
||||
Clock quartz.Clock
|
||||
|
||||
enqueuer notifications.Enqueuer
|
||||
logger slog.Logger
|
||||
@@ -39,19 +40,21 @@ type EnterpriseTemplateScheduleStore struct {
|
||||
|
||||
var _ agpl.TemplateScheduleStore = &EnterpriseTemplateScheduleStore{}
|
||||
|
||||
func NewEnterpriseTemplateScheduleStore(userQuietHoursStore *atomic.Pointer[agpl.UserQuietHoursScheduleStore], enqueuer notifications.Enqueuer, logger slog.Logger) *EnterpriseTemplateScheduleStore {
|
||||
func NewEnterpriseTemplateScheduleStore(userQuietHoursStore *atomic.Pointer[agpl.UserQuietHoursScheduleStore], enqueuer notifications.Enqueuer, logger slog.Logger, clock quartz.Clock) *EnterpriseTemplateScheduleStore {
|
||||
if clock == nil {
|
||||
clock = quartz.NewReal()
|
||||
}
|
||||
|
||||
return &EnterpriseTemplateScheduleStore{
|
||||
UserQuietHoursScheduleStore: userQuietHoursStore,
|
||||
Clock: clock,
|
||||
enqueuer: enqueuer,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *EnterpriseTemplateScheduleStore) now() time.Time {
|
||||
if s.TimeNowFn != nil {
|
||||
return s.TimeNowFn()
|
||||
}
|
||||
return dbtime.Now()
|
||||
return dbtime.Time(s.Clock.Now())
|
||||
}
|
||||
|
||||
// Get implements agpl.TemplateScheduleStore.
|
||||
@@ -164,7 +167,7 @@ func (s *EnterpriseTemplateScheduleStore) Set(ctx context.Context, db database.S
|
||||
|
||||
var dormantAt time.Time
|
||||
if opts.UpdateWorkspaceDormantAt {
|
||||
dormantAt = dbtime.Now()
|
||||
dormantAt = s.now()
|
||||
}
|
||||
|
||||
// If we updated the time_til_dormant_autodelete we need to update all the workspaces deleting_at
|
||||
@@ -205,8 +208,45 @@ func (s *EnterpriseTemplateScheduleStore) Set(ctx context.Context, db database.S
|
||||
return database.Template{}, err
|
||||
}
|
||||
|
||||
if opts.AutostartRequirement.DaysOfWeek != tpl.AutostartAllowedDays() {
|
||||
templateSchedule, err := s.Get(ctx, db, tpl.ID)
|
||||
if err != nil {
|
||||
return database.Template{}, xerrors.Errorf("get template schedule: %w", err)
|
||||
}
|
||||
|
||||
//nolint:gocritic // We need to be able to read information about all workspaces.
|
||||
workspaces, err := db.GetWorkspacesByTemplateID(dbauthz.AsSystemRestricted(ctx), tpl.ID)
|
||||
if err != nil {
|
||||
return database.Template{}, xerrors.Errorf("get workspaces by template id: %w", err)
|
||||
}
|
||||
|
||||
workspaceIDs := []uuid.UUID{}
|
||||
nextStartAts := []time.Time{}
|
||||
|
||||
for _, workspace := range workspaces {
|
||||
nextStartAt := time.Time{}
|
||||
if workspace.AutostartSchedule.Valid {
|
||||
next, err := agpl.NextAllowedAutostart(s.now(), workspace.AutostartSchedule.String, templateSchedule)
|
||||
if err == nil {
|
||||
nextStartAt = dbtime.Time(next.UTC())
|
||||
}
|
||||
}
|
||||
|
||||
workspaceIDs = append(workspaceIDs, workspace.ID)
|
||||
nextStartAts = append(nextStartAts, nextStartAt)
|
||||
}
|
||||
|
||||
//nolint:gocritic // We need to be able to update information about all workspaces.
|
||||
if err := db.BatchUpdateWorkspaceNextStartAt(dbauthz.AsSystemRestricted(ctx), database.BatchUpdateWorkspaceNextStartAtParams{
|
||||
IDs: workspaceIDs,
|
||||
NextStartAts: nextStartAts,
|
||||
}); err != nil {
|
||||
return database.Template{}, xerrors.Errorf("update workspace next start at: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, ws := range markedForDeletion {
|
||||
dormantTime := dbtime.Now().Add(opts.TimeTilDormantAutoDelete)
|
||||
dormantTime := s.now().Add(opts.TimeTilDormantAutoDelete)
|
||||
_, err = s.enqueuer.Enqueue(
|
||||
// nolint:gocritic // Need actor to enqueue notification
|
||||
dbauthz.AsNotifier(ctx),
|
||||
@@ -304,6 +344,23 @@ func (s *EnterpriseTemplateScheduleStore) updateWorkspaceBuild(ctx context.Conte
|
||||
return xerrors.Errorf("calculate new autostop for workspace %q: %w", workspace.ID, err)
|
||||
}
|
||||
|
||||
if workspace.AutostartSchedule.Valid {
|
||||
templateScheduleOptions, err := s.Get(ctx, db, workspace.TemplateID)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get template schedule options: %w", err)
|
||||
}
|
||||
|
||||
nextStartAt, _ := agpl.NextAutostart(s.now(), workspace.AutostartSchedule.String, templateScheduleOptions)
|
||||
|
||||
err = db.UpdateWorkspaceNextStartAt(ctx, database.UpdateWorkspaceNextStartAtParams{
|
||||
ID: workspace.ID,
|
||||
NextStartAt: sql.NullTime{Valid: true, Time: nextStartAt},
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("update workspace next start at: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 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".
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"github.com/coder/coder/v2/cryptorand"
|
||||
"github.com/coder/coder/v2/enterprise/coderd/schedule"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
"github.com/coder/quartz"
|
||||
)
|
||||
|
||||
func TestTemplateUpdateBuildDeadlines(t *testing.T) {
|
||||
@@ -283,11 +284,11 @@ func TestTemplateUpdateBuildDeadlines(t *testing.T) {
|
||||
userQuietHoursStorePtr := &atomic.Pointer[agplschedule.UserQuietHoursScheduleStore]{}
|
||||
userQuietHoursStorePtr.Store(&userQuietHoursStore)
|
||||
|
||||
clock := quartz.NewMock(t)
|
||||
clock.Set(c.now)
|
||||
|
||||
// Set the template policy.
|
||||
templateScheduleStore := schedule.NewEnterpriseTemplateScheduleStore(userQuietHoursStorePtr, notifications.NewNoopEnqueuer(), logger)
|
||||
templateScheduleStore.TimeNowFn = func() time.Time {
|
||||
return c.now
|
||||
}
|
||||
templateScheduleStore := schedule.NewEnterpriseTemplateScheduleStore(userQuietHoursStorePtr, notifications.NewNoopEnqueuer(), logger, clock)
|
||||
|
||||
autostopReq := agplschedule.TemplateAutostopRequirement{
|
||||
// Every day
|
||||
@@ -570,11 +571,11 @@ func TestTemplateUpdateBuildDeadlinesSkip(t *testing.T) {
|
||||
userQuietHoursStorePtr := &atomic.Pointer[agplschedule.UserQuietHoursScheduleStore]{}
|
||||
userQuietHoursStorePtr.Store(&userQuietHoursStore)
|
||||
|
||||
clock := quartz.NewMock(t)
|
||||
clock.Set(now)
|
||||
|
||||
// Set the template policy.
|
||||
templateScheduleStore := schedule.NewEnterpriseTemplateScheduleStore(userQuietHoursStorePtr, notifications.NewNoopEnqueuer(), logger)
|
||||
templateScheduleStore.TimeNowFn = func() time.Time {
|
||||
return now
|
||||
}
|
||||
templateScheduleStore := schedule.NewEnterpriseTemplateScheduleStore(userQuietHoursStorePtr, notifications.NewNoopEnqueuer(), logger, clock)
|
||||
_, err = templateScheduleStore.Set(ctx, db, template, agplschedule.TemplateScheduleOptions{
|
||||
UserAutostartEnabled: false,
|
||||
UserAutostopEnabled: false,
|
||||
@@ -682,8 +683,7 @@ func TestNotifications(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
userQuietHoursStorePtr := &atomic.Pointer[agplschedule.UserQuietHoursScheduleStore]{}
|
||||
userQuietHoursStorePtr.Store(&userQuietHoursStore)
|
||||
templateScheduleStore := schedule.NewEnterpriseTemplateScheduleStore(userQuietHoursStorePtr, ¬ifyEnq, logger)
|
||||
templateScheduleStore.TimeNowFn = time.Now
|
||||
templateScheduleStore := schedule.NewEnterpriseTemplateScheduleStore(userQuietHoursStorePtr, ¬ifyEnq, logger, nil)
|
||||
|
||||
// Lower the dormancy TTL to ensure the schedule recalculates deadlines and
|
||||
// triggers notifications.
|
||||
|
||||
@@ -689,7 +689,7 @@ func TestTemplates(t *testing.T) {
|
||||
client, user := coderdenttest.New(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
IncludeProvisionerDaemon: true,
|
||||
TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore(), notifications.NewNoopEnqueuer(), logger),
|
||||
TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore(), notifications.NewNoopEnqueuer(), logger, nil),
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
@@ -739,7 +739,7 @@ func TestTemplates(t *testing.T) {
|
||||
owner, first := coderdenttest.New(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
IncludeProvisionerDaemon: true,
|
||||
TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore(), notifications.NewNoopEnqueuer(), logger),
|
||||
TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore(), notifications.NewNoopEnqueuer(), logger, nil),
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
|
||||
@@ -2,11 +2,13 @@ package coderd_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"net/http"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"cdr.dev/slog"
|
||||
@@ -17,8 +19,10 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/autobuild"
|
||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
"github.com/coder/coder/v2/coderd/database/dbfake"
|
||||
"github.com/coder/coder/v2/coderd/database/dbtestutil"
|
||||
"github.com/coder/coder/v2/coderd/database/dbtime"
|
||||
"github.com/coder/coder/v2/coderd/notifications"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
agplschedule "github.com/coder/coder/v2/coderd/schedule"
|
||||
@@ -32,6 +36,7 @@ import (
|
||||
"github.com/coder/coder/v2/enterprise/coderd/schedule"
|
||||
"github.com/coder/coder/v2/provisioner/echo"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
"github.com/coder/quartz"
|
||||
)
|
||||
|
||||
// agplUserQuietHoursScheduleStore is passed to
|
||||
@@ -295,7 +300,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
|
||||
AutobuildTicker: ticker,
|
||||
IncludeProvisionerDaemon: true,
|
||||
AutobuildStats: statCh,
|
||||
TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore(), notifications.NewNoopEnqueuer(), logger),
|
||||
TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore(), notifications.NewNoopEnqueuer(), logger, nil),
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{codersdk.FeatureAdvancedTemplateScheduling: 1},
|
||||
@@ -342,7 +347,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
|
||||
AutobuildTicker: ticker,
|
||||
IncludeProvisionerDaemon: true,
|
||||
AutobuildStats: statCh,
|
||||
TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore(), notifications.NewNoopEnqueuer(), logger),
|
||||
TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore(), notifications.NewNoopEnqueuer(), logger, nil),
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{codersdk.FeatureAdvancedTemplateScheduling: 1},
|
||||
@@ -388,7 +393,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
|
||||
AutobuildTicker: ticker,
|
||||
IncludeProvisionerDaemon: true,
|
||||
AutobuildStats: statCh,
|
||||
TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore(), notifications.NewNoopEnqueuer(), logger),
|
||||
TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore(), notifications.NewNoopEnqueuer(), logger, nil),
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{codersdk.FeatureAdvancedTemplateScheduling: 1},
|
||||
@@ -432,7 +437,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
|
||||
Options: &coderdtest.Options{
|
||||
AutobuildTicker: ticker,
|
||||
AutobuildStats: statCh,
|
||||
TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore(), notifications.NewNoopEnqueuer(), logger),
|
||||
TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore(), notifications.NewNoopEnqueuer(), logger, nil),
|
||||
Auditor: auditRecorder,
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
@@ -527,7 +532,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
|
||||
Options: &coderdtest.Options{
|
||||
AutobuildTicker: ticker,
|
||||
AutobuildStats: statCh,
|
||||
TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore(), notifications.NewNoopEnqueuer(), logger),
|
||||
TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore(), notifications.NewNoopEnqueuer(), logger, nil),
|
||||
Database: db,
|
||||
Pubsub: pubsub,
|
||||
Auditor: auditor,
|
||||
@@ -585,7 +590,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
|
||||
AutobuildTicker: ticker,
|
||||
IncludeProvisionerDaemon: true,
|
||||
AutobuildStats: statCh,
|
||||
TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore(), notifications.NewNoopEnqueuer(), logger),
|
||||
TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore(), notifications.NewNoopEnqueuer(), logger, nil),
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{codersdk.FeatureAdvancedTemplateScheduling: 1},
|
||||
@@ -628,7 +633,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
|
||||
AutobuildTicker: ticker,
|
||||
IncludeProvisionerDaemon: true,
|
||||
AutobuildStats: statCh,
|
||||
TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore(), notifications.NewNoopEnqueuer(), logger),
|
||||
TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore(), notifications.NewNoopEnqueuer(), logger, nil),
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{codersdk.FeatureAdvancedTemplateScheduling: 1},
|
||||
@@ -671,7 +676,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
|
||||
AutobuildTicker: ticker,
|
||||
IncludeProvisionerDaemon: true,
|
||||
AutobuildStats: statCh,
|
||||
TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore(), notifications.NewNoopEnqueuer(), logger),
|
||||
TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore(), notifications.NewNoopEnqueuer(), logger, nil),
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{codersdk.FeatureAdvancedTemplateScheduling: 1},
|
||||
@@ -725,7 +730,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
|
||||
AutobuildTicker: ticker,
|
||||
IncludeProvisionerDaemon: true,
|
||||
AutobuildStats: statCh,
|
||||
TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore(), notifications.NewNoopEnqueuer(), logger),
|
||||
TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore(), notifications.NewNoopEnqueuer(), logger, nil),
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{codersdk.FeatureAdvancedTemplateScheduling: 1},
|
||||
@@ -797,7 +802,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
|
||||
AutobuildTicker: ticker,
|
||||
IncludeProvisionerDaemon: true,
|
||||
AutobuildStats: statCh,
|
||||
TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore(), notifications.NewNoopEnqueuer(), logger),
|
||||
TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore(), notifications.NewNoopEnqueuer(), logger, nil),
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{codersdk.FeatureAdvancedTemplateScheduling: 1},
|
||||
@@ -861,7 +866,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
|
||||
AutobuildTicker: tickCh,
|
||||
IncludeProvisionerDaemon: true,
|
||||
AutobuildStats: statsCh,
|
||||
TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore(), notifications.NewNoopEnqueuer(), logger),
|
||||
TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore(), notifications.NewNoopEnqueuer(), logger, nil),
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{codersdk.FeatureAdvancedTemplateScheduling: 1},
|
||||
@@ -941,7 +946,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
|
||||
AutobuildTicker: ticker,
|
||||
IncludeProvisionerDaemon: true,
|
||||
AutobuildStats: statCh,
|
||||
TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore(), notifications.NewNoopEnqueuer(), logger),
|
||||
TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore(), notifications.NewNoopEnqueuer(), logger, nil),
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{codersdk.FeatureAdvancedTemplateScheduling: 1},
|
||||
@@ -1027,7 +1032,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
|
||||
AutobuildTicker: tickCh,
|
||||
IncludeProvisionerDaemon: true,
|
||||
AutobuildStats: statsCh,
|
||||
TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore(), notifications.NewNoopEnqueuer(), logger),
|
||||
TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore(), notifications.NewNoopEnqueuer(), logger, nil),
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{codersdk.FeatureAccessControl: 1},
|
||||
@@ -1102,6 +1107,245 @@ func TestWorkspaceAutobuild(t *testing.T) {
|
||||
ws = coderdtest.MustWorkspace(t, client, ws.ID)
|
||||
require.Equal(t, version2.ID, ws.LatestBuild.TemplateVersionID)
|
||||
})
|
||||
|
||||
t.Run("NextStartAtIsValid", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
tickCh = make(chan time.Time)
|
||||
statsCh = make(chan autobuild.Stats)
|
||||
clock = quartz.NewMock(t)
|
||||
)
|
||||
|
||||
// Set the clock to 8AM Monday, 1st January, 2024 to keep
|
||||
// this test deterministic.
|
||||
clock.Set(time.Date(2024, 1, 1, 8, 0, 0, 0, time.UTC))
|
||||
|
||||
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug)
|
||||
client, user := coderdenttest.New(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
AutobuildTicker: tickCh,
|
||||
IncludeProvisionerDaemon: true,
|
||||
AutobuildStats: statsCh,
|
||||
Logger: &logger,
|
||||
Clock: clock,
|
||||
TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore(), notifications.NewNoopEnqueuer(), logger, nil),
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{codersdk.FeatureAdvancedTemplateScheduling: 1},
|
||||
},
|
||||
})
|
||||
|
||||
version1 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version1.ID)
|
||||
|
||||
// First create a template that only supports Monday-Friday
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version1.ID, func(ctr *codersdk.CreateTemplateRequest) {
|
||||
ctr.AutostartRequirement = &codersdk.TemplateAutostartRequirement{DaysOfWeek: codersdk.BitmapToWeekdays(0b00011111)}
|
||||
})
|
||||
require.Equal(t, version1.ID, template.ActiveVersionID)
|
||||
|
||||
// Then create a workspace with a schedule Sunday-Saturday
|
||||
sched, err := cron.Weekly("CRON_TZ=UTC 0 9 * * 0-6")
|
||||
require.NoError(t, err)
|
||||
ws := coderdtest.CreateWorkspace(t, client, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
|
||||
cwr.AutostartSchedule = ptr.Ref(sched.String())
|
||||
})
|
||||
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID)
|
||||
ws = coderdtest.MustTransitionWorkspace(t, client, ws.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
|
||||
next := ws.LatestBuild.CreatedAt
|
||||
|
||||
// For each day of the week (Monday-Sunday)
|
||||
// We iterate through each day of the week to ensure the behavior of each
|
||||
// day of the week is as expected.
|
||||
for range 7 {
|
||||
next = sched.Next(next)
|
||||
|
||||
clock.Set(next)
|
||||
tickCh <- next
|
||||
stats := <-statsCh
|
||||
ws = coderdtest.MustWorkspace(t, client, ws.ID)
|
||||
|
||||
// Our cron schedule specifies Sunday-Saturday but the template only allows
|
||||
// Monday-Friday so we expect there to be no transitions on the weekend.
|
||||
if next.Weekday() == time.Saturday || next.Weekday() == time.Sunday {
|
||||
assert.Len(t, stats.Errors, 0)
|
||||
assert.Len(t, stats.Transitions, 0)
|
||||
|
||||
ws = coderdtest.MustWorkspace(t, client, ws.ID)
|
||||
} else {
|
||||
assert.Len(t, stats.Errors, 0)
|
||||
assert.Len(t, stats.Transitions, 1)
|
||||
assert.Contains(t, stats.Transitions, ws.ID)
|
||||
assert.Equal(t, database.WorkspaceTransitionStart, stats.Transitions[ws.ID])
|
||||
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID)
|
||||
ws = coderdtest.MustTransitionWorkspace(t, client, ws.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
|
||||
}
|
||||
|
||||
// Ensure that there is a valid next start at and that is is after
|
||||
// the preivous start.
|
||||
require.NotNil(t, ws.NextStartAt)
|
||||
require.Greater(t, *ws.NextStartAt, next)
|
||||
|
||||
// Our autostart requirement disallows sundays and saturdays so
|
||||
// the next start at should never land on these days.
|
||||
require.NotEqual(t, time.Saturday, ws.NextStartAt.Weekday())
|
||||
require.NotEqual(t, time.Sunday, ws.NextStartAt.Weekday())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("NextStartAtIsUpdatedWhenTemplateAutostartRequirementsChange", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
tickCh = make(chan time.Time)
|
||||
statsCh = make(chan autobuild.Stats)
|
||||
clock = quartz.NewMock(t)
|
||||
)
|
||||
|
||||
// Set the clock to 8AM Monday, 1st January, 2024 to keep
|
||||
// this test deterministic.
|
||||
clock.Set(time.Date(2024, 1, 1, 8, 0, 0, 0, time.UTC))
|
||||
|
||||
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug)
|
||||
templateScheduleStore := schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore(), notifications.NewNoopEnqueuer(), logger, nil)
|
||||
templateScheduleStore.Clock = clock
|
||||
client, user := coderdenttest.New(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
AutobuildTicker: tickCh,
|
||||
IncludeProvisionerDaemon: true,
|
||||
AutobuildStats: statsCh,
|
||||
Logger: &logger,
|
||||
Clock: clock,
|
||||
TemplateScheduleStore: templateScheduleStore,
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{codersdk.FeatureAdvancedTemplateScheduling: 1},
|
||||
},
|
||||
})
|
||||
|
||||
version1 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version1.ID)
|
||||
|
||||
// First create a template that only supports Monday-Friday
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version1.ID, func(ctr *codersdk.CreateTemplateRequest) {
|
||||
ctr.AllowUserAutostart = ptr.Ref(true)
|
||||
ctr.AutostartRequirement = &codersdk.TemplateAutostartRequirement{DaysOfWeek: codersdk.BitmapToWeekdays(0b00011111)}
|
||||
})
|
||||
require.Equal(t, version1.ID, template.ActiveVersionID)
|
||||
|
||||
// Then create a workspace with a schedule Monday-Friday
|
||||
sched, err := cron.Weekly("CRON_TZ=UTC 0 9 * * 1-5")
|
||||
require.NoError(t, err)
|
||||
ws := coderdtest.CreateWorkspace(t, client, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
|
||||
cwr.AutostartSchedule = ptr.Ref(sched.String())
|
||||
})
|
||||
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID)
|
||||
ws = coderdtest.MustTransitionWorkspace(t, client, ws.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
|
||||
|
||||
// Our next start at should be Monday
|
||||
require.NotNil(t, ws.NextStartAt)
|
||||
require.Equal(t, time.Monday, ws.NextStartAt.Weekday())
|
||||
|
||||
// Now update the template to only allow Tuesday-Friday
|
||||
coderdtest.UpdateTemplateMeta(t, client, template.ID, codersdk.UpdateTemplateMeta{
|
||||
AutostartRequirement: &codersdk.TemplateAutostartRequirement{
|
||||
DaysOfWeek: codersdk.BitmapToWeekdays(0b00011110),
|
||||
},
|
||||
})
|
||||
|
||||
// Verify that our next start at has been updated to Tuesday
|
||||
ws = coderdtest.MustWorkspace(t, client, ws.ID)
|
||||
require.NotNil(t, ws.NextStartAt)
|
||||
require.Equal(t, time.Tuesday, ws.NextStartAt.Weekday())
|
||||
})
|
||||
|
||||
t.Run("NextStartAtIsNullifiedOnScheduleChange", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if !dbtestutil.WillUsePostgres() {
|
||||
t.Skip("this test uses triggers so does not work with dbmem.go")
|
||||
}
|
||||
|
||||
var (
|
||||
tickCh = make(chan time.Time)
|
||||
statsCh = make(chan autobuild.Stats)
|
||||
)
|
||||
|
||||
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug)
|
||||
client, db, user := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
AutobuildTicker: tickCh,
|
||||
IncludeProvisionerDaemon: true,
|
||||
AutobuildStats: statsCh,
|
||||
Logger: &logger,
|
||||
TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore(), notifications.NewNoopEnqueuer(), logger, nil),
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{codersdk.FeatureAdvancedTemplateScheduling: 1},
|
||||
},
|
||||
})
|
||||
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||
|
||||
// Create a template that allows autostart Monday-Sunday
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
|
||||
ctr.AutostartRequirement = &codersdk.TemplateAutostartRequirement{DaysOfWeek: codersdk.AllDaysOfWeek}
|
||||
})
|
||||
require.Equal(t, version.ID, template.ActiveVersionID)
|
||||
|
||||
// Create a workspace with a schedule Sunday-Saturday
|
||||
sched, err := cron.Weekly("CRON_TZ=UTC 0 9 * * 0-6")
|
||||
require.NoError(t, err)
|
||||
ws := coderdtest.CreateWorkspace(t, client, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
|
||||
cwr.AutostartSchedule = ptr.Ref(sched.String())
|
||||
})
|
||||
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID)
|
||||
ws = coderdtest.MustTransitionWorkspace(t, client, ws.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
|
||||
|
||||
// Check we have a 'NextStartAt'
|
||||
require.NotNil(t, ws.NextStartAt)
|
||||
|
||||
// Create a new slightly different cron schedule that could
|
||||
// potentially make NextStartAt invalid.
|
||||
sched, err = cron.Weekly("CRON_TZ=UTC 0 9 * * 1-6")
|
||||
require.NoError(t, err)
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
|
||||
// We want to test the database nullifies the NextStartAt so we
|
||||
// make a raw DB call here. We pass in NextStartAt here so we
|
||||
// can test the database will nullify it and not us.
|
||||
//nolint: gocritic // We need system context to modify this.
|
||||
err = db.UpdateWorkspaceAutostart(dbauthz.AsSystemRestricted(ctx), database.UpdateWorkspaceAutostartParams{
|
||||
ID: ws.ID,
|
||||
AutostartSchedule: sql.NullString{Valid: true, String: sched.String()},
|
||||
NextStartAt: sql.NullTime{Valid: true, Time: *ws.NextStartAt},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
ws = coderdtest.MustWorkspace(t, client, ws.ID)
|
||||
|
||||
// Check 'NextStartAt' has been nullified
|
||||
require.Nil(t, ws.NextStartAt)
|
||||
|
||||
// Now we let the lifecycle executor run. This should spot that the
|
||||
// NextStartAt is null and update it for us.
|
||||
next := dbtime.Now()
|
||||
tickCh <- next
|
||||
stats := <-statsCh
|
||||
assert.Len(t, stats.Errors, 0)
|
||||
assert.Len(t, stats.Transitions, 0)
|
||||
|
||||
// Ensure NextStartAt has been set, and is the expected value
|
||||
ws = coderdtest.MustWorkspace(t, client, ws.ID)
|
||||
require.NotNil(t, ws.NextStartAt)
|
||||
require.Equal(t, sched.Next(next), ws.NextStartAt.UTC())
|
||||
})
|
||||
}
|
||||
|
||||
func TestTemplateDoesNotAllowUserAutostop(t *testing.T) {
|
||||
@@ -1112,7 +1356,7 @@ func TestTemplateDoesNotAllowUserAutostop(t *testing.T) {
|
||||
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug)
|
||||
client := coderdtest.New(t, &coderdtest.Options{
|
||||
IncludeProvisionerDaemon: true,
|
||||
TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore(), notifications.NewNoopEnqueuer(), logger),
|
||||
TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore(), notifications.NewNoopEnqueuer(), logger, nil),
|
||||
})
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
@@ -1151,7 +1395,7 @@ func TestTemplateDoesNotAllowUserAutostop(t *testing.T) {
|
||||
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug)
|
||||
client := coderdtest.New(t, &coderdtest.Options{
|
||||
IncludeProvisionerDaemon: true,
|
||||
TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore(), notifications.NewNoopEnqueuer(), logger),
|
||||
TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore(), notifications.NewNoopEnqueuer(), logger, nil),
|
||||
})
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
@@ -1203,7 +1447,7 @@ func TestExecutorAutostartBlocked(t *testing.T) {
|
||||
AutobuildTicker: tickCh,
|
||||
IncludeProvisionerDaemon: true,
|
||||
AutobuildStats: statsCh,
|
||||
TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore(), notifications.NewNoopEnqueuer(), logger),
|
||||
TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore(), notifications.NewNoopEnqueuer(), logger, nil),
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{codersdk.FeatureAdvancedTemplateScheduling: 1},
|
||||
@@ -1225,9 +1469,9 @@ func TestExecutorAutostartBlocked(t *testing.T) {
|
||||
// Given: workspace is stopped
|
||||
workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
|
||||
|
||||
// When: the autobuild executor ticks way into the future
|
||||
// When: the autobuild executor ticks into the future
|
||||
go func() {
|
||||
tickCh <- workspace.LatestBuild.CreatedAt.Add(24 * time.Hour)
|
||||
tickCh <- workspace.LatestBuild.CreatedAt.Add(2 * time.Hour)
|
||||
close(tickCh)
|
||||
}()
|
||||
|
||||
@@ -1247,7 +1491,7 @@ func TestWorkspacesFiltering(t *testing.T) {
|
||||
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug)
|
||||
client, db, owner := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore(), notifications.NewNoopEnqueuer(), logger),
|
||||
TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore(), notifications.NewNoopEnqueuer(), logger, nil),
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{codersdk.FeatureAdvancedTemplateScheduling: 1},
|
||||
@@ -1362,7 +1606,7 @@ func TestWorkspaceLock(t *testing.T) {
|
||||
client, user = coderdenttest.New(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
IncludeProvisionerDaemon: true,
|
||||
TemplateScheduleStore: &schedule.EnterpriseTemplateScheduleStore{},
|
||||
TemplateScheduleStore: &schedule.EnterpriseTemplateScheduleStore{Clock: quartz.NewReal()},
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
@@ -1423,7 +1667,7 @@ func TestResolveAutostart(t *testing.T) {
|
||||
|
||||
ownerClient, db, owner := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
TemplateScheduleStore: &schedule.EnterpriseTemplateScheduleStore{},
|
||||
TemplateScheduleStore: &schedule.EnterpriseTemplateScheduleStore{Clock: quartz.NewReal()},
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
|
||||
Generated
+1
@@ -1847,6 +1847,7 @@ export interface Workspace {
|
||||
readonly automatic_updates: AutomaticUpdates;
|
||||
readonly allow_renames: boolean;
|
||||
readonly favorite: boolean;
|
||||
readonly next_start_at?: string;
|
||||
}
|
||||
|
||||
// From codersdk/workspaceagents.go
|
||||
|
||||
Reference in New Issue
Block a user