fix: update workspace TTL on template TTL change (#15761)

Relates to https://github.com/coder/coder/issues/15390

Currently when a user creates a workspace, their workspace's TTL is
determined by the template's default TTL. If the Coder instance is AGPL,
or if the template has disallowed the user from configuring autostop,
then it is not possible to change the workspace's TTL after creation.
Any changes to the template's default TTL only takes effect on _new_
workspaces.

This PR modifies the behaviour slightly so that on AGPL Coder, or on
enterprise when a template does not allow user's to configure their
workspace's TTL, updating the template's default TTL will also update
any workspace's TTL to match this value.
This commit is contained in:
Danielle Maywood
2024-12-06 11:01:39 +00:00
committed by GitHub
parent 67553a7bbe
commit 40624bf78b
12 changed files with 518 additions and 0 deletions
+11
View File
@@ -4138,6 +4138,17 @@ func (q *querier) UpdateWorkspacesDormantDeletingAtByTemplateID(ctx context.Cont
return q.db.UpdateWorkspacesDormantDeletingAtByTemplateID(ctx, arg)
}
func (q *querier) UpdateWorkspacesTTLByTemplateID(ctx context.Context, arg database.UpdateWorkspacesTTLByTemplateIDParams) error {
template, err := q.db.GetTemplateByID(ctx, arg.TemplateID)
if err != nil {
return xerrors.Errorf("get template by id: %w", err)
}
if err := q.authorizeContext(ctx, policy.ActionUpdate, template); err != nil {
return err
}
return q.db.UpdateWorkspacesTTLByTemplateID(ctx, arg)
}
func (q *querier) UpsertAnnouncementBanners(ctx context.Context, value string) error {
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceDeploymentConfig); err != nil {
return err
+6
View File
@@ -1017,6 +1017,12 @@ func (s *MethodTestSuite) TestTemplate() {
TemplateID: t1.ID,
}).Asserts(t1, policy.ActionUpdate)
}))
s.Run("UpdateWorkspacesTTLByTemplateID", s.Subtest(func(db database.Store, check *expects) {
t1 := dbgen.Template(s.T(), db, database.Template{})
check.Args(database.UpdateWorkspacesTTLByTemplateIDParams{
TemplateID: t1.ID,
}).Asserts(t1, policy.ActionUpdate)
}))
s.Run("UpdateTemplateActiveVersionByID", s.Subtest(func(db database.Store, check *expects) {
t1 := dbgen.Template(s.T(), db, database.Template{
ActiveVersionID: uuid.New(),
+20
View File
@@ -10192,6 +10192,26 @@ func (q *FakeQuerier) UpdateWorkspacesDormantDeletingAtByTemplateID(_ context.Co
return affectedRows, nil
}
func (q *FakeQuerier) UpdateWorkspacesTTLByTemplateID(_ context.Context, arg database.UpdateWorkspacesTTLByTemplateIDParams) error {
err := validateDatabaseType(arg)
if err != nil {
return err
}
q.mutex.Lock()
defer q.mutex.Unlock()
for i, ws := range q.workspaces {
if ws.TemplateID != arg.TemplateID {
continue
}
q.workspaces[i].Ttl = arg.Ttl
}
return nil
}
func (q *FakeQuerier) UpsertAnnouncementBanners(_ context.Context, data string) error {
q.mutex.RLock()
defer q.mutex.RUnlock()
@@ -2590,6 +2590,13 @@ func (m queryMetricsStore) UpdateWorkspacesDormantDeletingAtByTemplateID(ctx con
return r0, r1
}
func (m queryMetricsStore) UpdateWorkspacesTTLByTemplateID(ctx context.Context, arg database.UpdateWorkspacesTTLByTemplateIDParams) error {
start := time.Now()
r0 := m.s.UpdateWorkspacesTTLByTemplateID(ctx, arg)
m.queryLatencies.WithLabelValues("UpdateWorkspacesTTLByTemplateID").Observe(time.Since(start).Seconds())
return r0
}
func (m queryMetricsStore) UpsertAnnouncementBanners(ctx context.Context, value string) error {
start := time.Now()
r0 := m.s.UpsertAnnouncementBanners(ctx, value)
+14
View File
@@ -5486,6 +5486,20 @@ func (mr *MockStoreMockRecorder) UpdateWorkspacesDormantDeletingAtByTemplateID(a
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspacesDormantDeletingAtByTemplateID", reflect.TypeOf((*MockStore)(nil).UpdateWorkspacesDormantDeletingAtByTemplateID), arg0, arg1)
}
// UpdateWorkspacesTTLByTemplateID mocks base method.
func (m *MockStore) UpdateWorkspacesTTLByTemplateID(arg0 context.Context, arg1 database.UpdateWorkspacesTTLByTemplateIDParams) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateWorkspacesTTLByTemplateID", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateWorkspacesTTLByTemplateID indicates an expected call of UpdateWorkspacesTTLByTemplateID.
func (mr *MockStoreMockRecorder) UpdateWorkspacesTTLByTemplateID(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspacesTTLByTemplateID", reflect.TypeOf((*MockStore)(nil).UpdateWorkspacesTTLByTemplateID), arg0, arg1)
}
// UpsertAnnouncementBanners mocks base method.
func (m *MockStore) UpsertAnnouncementBanners(arg0 context.Context, arg1 string) error {
m.ctrl.T.Helper()
+1
View File
@@ -501,6 +501,7 @@ type sqlcQuerier interface {
UpdateWorkspaceProxyDeleted(ctx context.Context, arg UpdateWorkspaceProxyDeletedParams) error
UpdateWorkspaceTTL(ctx context.Context, arg UpdateWorkspaceTTLParams) error
UpdateWorkspacesDormantDeletingAtByTemplateID(ctx context.Context, arg UpdateWorkspacesDormantDeletingAtByTemplateIDParams) ([]WorkspaceTable, error)
UpdateWorkspacesTTLByTemplateID(ctx context.Context, arg UpdateWorkspacesTTLByTemplateIDParams) error
UpsertAnnouncementBanners(ctx context.Context, value string) error
UpsertAppSecurityKey(ctx context.Context, value string) error
UpsertApplicationName(ctx context.Context, value string) error
+19
View File
@@ -16238,6 +16238,25 @@ func (q *sqlQuerier) UpdateWorkspacesDormantDeletingAtByTemplateID(ctx context.C
return items, nil
}
const updateWorkspacesTTLByTemplateID = `-- name: UpdateWorkspacesTTLByTemplateID :exec
UPDATE
workspaces
SET
ttl = $2
WHERE
template_id = $1
`
type UpdateWorkspacesTTLByTemplateIDParams struct {
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
Ttl sql.NullInt64 `db:"ttl" json:"ttl"`
}
func (q *sqlQuerier) UpdateWorkspacesTTLByTemplateID(ctx context.Context, arg UpdateWorkspacesTTLByTemplateIDParams) error {
_, err := q.db.ExecContext(ctx, updateWorkspacesTTLByTemplateID, arg.TemplateID, arg.Ttl)
return err
}
const getWorkspaceAgentScriptsByAgentIDs = `-- name: GetWorkspaceAgentScriptsByAgentIDs :many
SELECT workspace_agent_id, log_source_id, log_path, created_at, script, cron, start_blocks_login, run_on_start, run_on_stop, timeout_seconds, display_name, id FROM workspace_agent_scripts WHERE workspace_agent_id = ANY($1 :: uuid [ ])
`
+8
View File
@@ -501,6 +501,14 @@ SET
WHERE
id = $1;
-- name: UpdateWorkspacesTTLByTemplateID :exec
UPDATE
workspaces
SET
ttl = $2
WHERE
template_id = $1;
-- name: UpdateWorkspaceLastUsedAt :exec
UPDATE
workspaces
+18
View File
@@ -2,6 +2,7 @@ package schedule
import (
"context"
"database/sql"
"time"
"github.com/google/uuid"
@@ -228,6 +229,23 @@ func (*agplTemplateScheduleStore) Set(ctx context.Context, db database.Store, tp
return xerrors.Errorf("update template schedule: %w", err)
}
// Users running the AGPL version are unable to customize their workspaces
// autostop, so we want to keep their workspaces in track with any template
// TTL changes.
if tpl.DefaultTTL != int64(opts.DefaultTTL) {
var ttl sql.NullInt64
if opts.DefaultTTL != 0 {
ttl = sql.NullInt64{Valid: true, Int64: int64(opts.DefaultTTL)}
}
if err = db.UpdateWorkspacesTTLByTemplateID(ctx, database.UpdateWorkspacesTTLByTemplateIDParams{
TemplateID: tpl.ID,
Ttl: ttl,
}); err != nil {
return xerrors.Errorf("update workspace ttl by template id %q: %w", tpl.ID, err)
}
}
template, err = db.GetTemplateByID(ctx, tpl.ID)
if err != nil {
return xerrors.Errorf("fetch updated template: %w", err)
+150
View File
@@ -0,0 +1,150 @@
package schedule_test
import (
"database/sql"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbgen"
"github.com/coder/coder/v2/coderd/database/dbtestutil"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/schedule"
"github.com/coder/coder/v2/testutil"
)
func TestTemplateTTL(t *testing.T) {
t.Parallel()
tests := []struct {
name string
fromTTL time.Duration
toTTL time.Duration
expected sql.NullInt64
}{
{
name: "ModifyTTLDurationDown",
fromTTL: 24 * time.Hour,
toTTL: 1 * time.Hour,
expected: sql.NullInt64{Valid: true, Int64: int64(1 * time.Hour)},
},
{
name: "ModifyTTLDurationUp",
fromTTL: 24 * time.Hour,
toTTL: 36 * time.Hour,
expected: sql.NullInt64{Valid: true, Int64: int64(36 * time.Hour)},
},
{
name: "ModifyTTLDurationSame",
fromTTL: 24 * time.Hour,
toTTL: 24 * time.Hour,
expected: sql.NullInt64{Valid: true, Int64: int64(24 * time.Hour)},
},
{
name: "DisableTTL",
fromTTL: 24 * time.Hour,
toTTL: 0,
expected: sql.NullInt64{},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var (
db, _ = dbtestutil.NewDB(t)
ctx = testutil.Context(t, testutil.WaitLong)
user = dbgen.User(t, db, database.User{})
file = dbgen.File(t, db, database.File{CreatedBy: user.ID})
// Create first template
templateJob = dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
FileID: file.ID,
InitiatorID: user.ID,
Tags: database.StringMap{"foo": "bar"},
})
templateVersion = dbgen.TemplateVersion(t, db, database.TemplateVersion{
CreatedBy: user.ID,
JobID: templateJob.ID,
OrganizationID: templateJob.OrganizationID,
})
template = dbgen.Template(t, db, database.Template{
ActiveVersionID: templateVersion.ID,
CreatedBy: user.ID,
OrganizationID: templateJob.OrganizationID,
})
// Create second template
otherTTL = tt.fromTTL + 6*time.Hour
otherTemplateJob = dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
FileID: file.ID,
InitiatorID: user.ID,
Tags: database.StringMap{"foo": "bar"},
})
otherTemplateVersion = dbgen.TemplateVersion(t, db, database.TemplateVersion{
CreatedBy: user.ID,
JobID: otherTemplateJob.ID,
OrganizationID: otherTemplateJob.OrganizationID,
})
otherTemplate = dbgen.Template(t, db, database.Template{
ActiveVersionID: otherTemplateVersion.ID,
CreatedBy: user.ID,
OrganizationID: otherTemplateJob.OrganizationID,
})
)
templateScheduleStore := schedule.NewAGPLTemplateScheduleStore()
// Set both template's default TTL
template, err := templateScheduleStore.Set(ctx, db, template, schedule.TemplateScheduleOptions{
DefaultTTL: tt.fromTTL,
})
require.NoError(t, err)
otherTemplate, err = templateScheduleStore.Set(ctx, db, otherTemplate, schedule.TemplateScheduleOptions{
DefaultTTL: otherTTL,
})
require.NoError(t, err)
// We create two workspaces here, one with the template we're modifying, the
// other with a different template. We want to ensure we only modify one
// of the workspaces.
workspace := dbgen.Workspace(t, db, database.WorkspaceTable{
OwnerID: user.ID,
TemplateID: template.ID,
OrganizationID: templateJob.OrganizationID,
LastUsedAt: dbtime.Now(),
Ttl: sql.NullInt64{Valid: true, Int64: int64(tt.fromTTL)},
})
otherWorkspace := dbgen.Workspace(t, db, database.WorkspaceTable{
OwnerID: user.ID,
TemplateID: otherTemplate.ID,
OrganizationID: otherTemplateJob.OrganizationID,
LastUsedAt: dbtime.Now(),
Ttl: sql.NullInt64{Valid: true, Int64: int64(otherTTL)},
})
// Ensure the workspace's start with the correct TTLs
require.Equal(t, sql.NullInt64{Valid: true, Int64: int64(tt.fromTTL)}, workspace.Ttl)
require.Equal(t, sql.NullInt64{Valid: true, Int64: int64(otherTTL)}, otherWorkspace.Ttl)
// Update _only_ the primary template's TTL
_, err = templateScheduleStore.Set(ctx, db, template, schedule.TemplateScheduleOptions{
DefaultTTL: tt.toTTL,
})
require.NoError(t, err)
// Verify the primary workspace's TTL has been updated.
ws, err := db.GetWorkspaceByID(ctx, workspace.ID)
require.NoError(t, err)
require.Equal(t, tt.expected, ws.Ttl)
// Verify that the other workspace's TTL has not been touched.
ws, err = db.GetWorkspaceByID(ctx, otherWorkspace.ID)
require.NoError(t, err)
require.Equal(t, sql.NullInt64{Valid: true, Int64: int64(otherTTL)}, ws.Ttl)
})
}
}
+17
View File
@@ -195,6 +195,23 @@ func (s *EnterpriseTemplateScheduleStore) Set(ctx context.Context, db database.S
return xerrors.Errorf("get updated template schedule: %w", err)
}
// Update all workspace's TTL using this template if either of the following:
// - The template's AllowUserAutostop has just been disabled
// - The template's TTL has been modified and AllowUserAutostop is disabled
if !opts.UserAutostopEnabled && (tpl.AllowUserAutostop || int64(opts.DefaultTTL) != tpl.DefaultTTL) {
var ttl sql.NullInt64
if opts.DefaultTTL != 0 {
ttl = sql.NullInt64{Valid: true, Int64: int64(opts.DefaultTTL)}
}
if err = tx.UpdateWorkspacesTTLByTemplateID(ctx, database.UpdateWorkspacesTTLByTemplateIDParams{
TemplateID: template.ID,
Ttl: ttl,
}); err != nil {
return xerrors.Errorf("update workspaces ttl by template id %q: %w", template.ID, err)
}
}
// Recalculate max_deadline and deadline for all running workspace
// builds on this template.
err = s.updateWorkspaceBuilds(ctx, tx, template)
+247
View File
@@ -19,6 +19,7 @@ import (
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/database/dbgen"
"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/notifications/notificationstest"
agplschedule "github.com/coder/coder/v2/coderd/schedule"
@@ -708,6 +709,252 @@ func TestNotifications(t *testing.T) {
})
}
func TestTemplateTTL(t *testing.T) {
t.Parallel()
tests := []struct {
name string
allowUserAutostop bool
fromTTL time.Duration
toTTL time.Duration
expected sql.NullInt64
}{
{
name: "AllowUserAutostopFalse/ModifyTTLDurationDown",
allowUserAutostop: false,
fromTTL: 24 * time.Hour,
toTTL: 1 * time.Hour,
expected: sql.NullInt64{Valid: true, Int64: int64(1 * time.Hour)},
},
{
name: "AllowUserAutostopFalse/ModifyTTLDurationUp",
allowUserAutostop: false,
fromTTL: 24 * time.Hour,
toTTL: 36 * time.Hour,
expected: sql.NullInt64{Valid: true, Int64: int64(36 * time.Hour)},
},
{
name: "AllowUserAutostopFalse/ModifyTTLDurationSame",
allowUserAutostop: false,
fromTTL: 24 * time.Hour,
toTTL: 24 * time.Hour,
expected: sql.NullInt64{Valid: true, Int64: int64(24 * time.Hour)},
},
{
name: "AllowUserAutostopFalse/DisableTTL",
allowUserAutostop: false,
fromTTL: 24 * time.Hour,
toTTL: 0,
expected: sql.NullInt64{},
},
{
name: "AllowUserAutostopTrue/ModifyTTLDurationDown",
allowUserAutostop: true,
fromTTL: 24 * time.Hour,
toTTL: 1 * time.Hour,
expected: sql.NullInt64{Valid: true, Int64: int64(24 * time.Hour)},
},
{
name: "AllowUserAutostopTrue/ModifyTTLDurationUp",
allowUserAutostop: true,
fromTTL: 24 * time.Hour,
toTTL: 36 * time.Hour,
expected: sql.NullInt64{Valid: true, Int64: int64(24 * time.Hour)},
},
{
name: "AllowUserAutostopTrue/ModifyTTLDurationSame",
allowUserAutostop: true,
fromTTL: 24 * time.Hour,
toTTL: 24 * time.Hour,
expected: sql.NullInt64{Valid: true, Int64: int64(24 * time.Hour)},
},
{
name: "AllowUserAutostopTrue/DisableTTL",
allowUserAutostop: true,
fromTTL: 24 * time.Hour,
toTTL: 0,
expected: sql.NullInt64{Valid: true, Int64: int64(24 * time.Hour)},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var (
logger = slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug)
db, _ = dbtestutil.NewDB(t)
ctx = testutil.Context(t, testutil.WaitLong)
user = dbgen.User(t, db, database.User{})
file = dbgen.File(t, db, database.File{CreatedBy: user.ID})
// Create first template
templateJob = dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
FileID: file.ID,
InitiatorID: user.ID,
Tags: database.StringMap{"foo": "bar"},
})
templateVersion = dbgen.TemplateVersion(t, db, database.TemplateVersion{
CreatedBy: user.ID,
JobID: templateJob.ID,
OrganizationID: templateJob.OrganizationID,
})
template = dbgen.Template(t, db, database.Template{
ActiveVersionID: templateVersion.ID,
CreatedBy: user.ID,
OrganizationID: templateJob.OrganizationID,
AllowUserAutostop: false,
})
// Create second template
otherTTL = tt.fromTTL + 6*time.Hour
otherTemplateJob = dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
FileID: file.ID,
InitiatorID: user.ID,
Tags: database.StringMap{"foo": "bar"},
})
otherTemplateVersion = dbgen.TemplateVersion(t, db, database.TemplateVersion{
CreatedBy: user.ID,
JobID: otherTemplateJob.ID,
OrganizationID: otherTemplateJob.OrganizationID,
})
otherTemplate = dbgen.Template(t, db, database.Template{
ActiveVersionID: otherTemplateVersion.ID,
CreatedBy: user.ID,
OrganizationID: otherTemplateJob.OrganizationID,
AllowUserAutostop: false,
})
)
// Setup the template schedule store
notifyEnq := notifications.NewNoopEnqueuer()
const userQuietHoursSchedule = "CRON_TZ=UTC 0 0 * * *" // midnight UTC
userQuietHoursStore, err := schedule.NewEnterpriseUserQuietHoursScheduleStore(userQuietHoursSchedule, true)
require.NoError(t, err)
userQuietHoursStorePtr := &atomic.Pointer[agplschedule.UserQuietHoursScheduleStore]{}
userQuietHoursStorePtr.Store(&userQuietHoursStore)
templateScheduleStore := schedule.NewEnterpriseTemplateScheduleStore(userQuietHoursStorePtr, notifyEnq, logger, nil)
// Set both template's default TTL
template, err = templateScheduleStore.Set(ctx, db, template, agplschedule.TemplateScheduleOptions{
DefaultTTL: tt.fromTTL,
})
require.NoError(t, err)
otherTemplate, err = templateScheduleStore.Set(ctx, db, otherTemplate, agplschedule.TemplateScheduleOptions{
DefaultTTL: otherTTL,
})
require.NoError(t, err)
// We create two workspaces here, one with the template we're modifying, the
// other with a different template. We want to ensure we only modify one
// of the workspaces.
workspace := dbgen.Workspace(t, db, database.WorkspaceTable{
OwnerID: user.ID,
TemplateID: template.ID,
OrganizationID: templateJob.OrganizationID,
LastUsedAt: dbtime.Now(),
Ttl: sql.NullInt64{Valid: true, Int64: int64(tt.fromTTL)},
})
otherWorkspace := dbgen.Workspace(t, db, database.WorkspaceTable{
OwnerID: user.ID,
TemplateID: otherTemplate.ID,
OrganizationID: otherTemplateJob.OrganizationID,
LastUsedAt: dbtime.Now(),
Ttl: sql.NullInt64{Valid: true, Int64: int64(otherTTL)},
})
// Ensure the workspace's start with the correct TTLs
require.Equal(t, sql.NullInt64{Valid: true, Int64: int64(tt.fromTTL)}, workspace.Ttl)
require.Equal(t, sql.NullInt64{Valid: true, Int64: int64(otherTTL)}, otherWorkspace.Ttl)
// Update _only_ the primary template's TTL
_, err = templateScheduleStore.Set(ctx, db, template, agplschedule.TemplateScheduleOptions{
UserAutostopEnabled: tt.allowUserAutostop,
DefaultTTL: tt.toTTL,
})
require.NoError(t, err)
// Verify the primary workspace's TTL is what we expect
ws, err := db.GetWorkspaceByID(ctx, workspace.ID)
require.NoError(t, err)
require.Equal(t, tt.expected, ws.Ttl)
// Verify we haven't changed the other workspace's TTL
ws, err = db.GetWorkspaceByID(ctx, otherWorkspace.ID)
require.NoError(t, err)
require.Equal(t, sql.NullInt64{Valid: true, Int64: int64(otherTTL)}, ws.Ttl)
})
}
t.Run("WorkspaceTTLUpdatedWhenAllowUserAutostopGetsDisabled", func(t *testing.T) {
t.Parallel()
var (
logger = slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug)
db, _ = dbtestutil.NewDB(t)
ctx = testutil.Context(t, testutil.WaitLong)
user = dbgen.User(t, db, database.User{})
file = dbgen.File(t, db, database.File{CreatedBy: user.ID})
// Create first template
templateJob = dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
FileID: file.ID,
InitiatorID: user.ID,
Tags: database.StringMap{"foo": "bar"},
})
templateVersion = dbgen.TemplateVersion(t, db, database.TemplateVersion{
CreatedBy: user.ID,
JobID: templateJob.ID,
OrganizationID: templateJob.OrganizationID,
})
template = dbgen.Template(t, db, database.Template{
ActiveVersionID: templateVersion.ID,
CreatedBy: user.ID,
OrganizationID: templateJob.OrganizationID,
})
)
// Setup the template schedule store
notifyEnq := notifications.NewNoopEnqueuer()
const userQuietHoursSchedule = "CRON_TZ=UTC 0 0 * * *" // midnight UTC
userQuietHoursStore, err := schedule.NewEnterpriseUserQuietHoursScheduleStore(userQuietHoursSchedule, true)
require.NoError(t, err)
userQuietHoursStorePtr := &atomic.Pointer[agplschedule.UserQuietHoursScheduleStore]{}
userQuietHoursStorePtr.Store(&userQuietHoursStore)
templateScheduleStore := schedule.NewEnterpriseTemplateScheduleStore(userQuietHoursStorePtr, notifyEnq, logger, nil)
// Enable AllowUserAutostop
template, err = templateScheduleStore.Set(ctx, db, template, agplschedule.TemplateScheduleOptions{
DefaultTTL: 24 * time.Hour,
UserAutostopEnabled: true,
})
require.NoError(t, err)
// Create a workspace with a TTL different than the template's default TTL
workspace := dbgen.Workspace(t, db, database.WorkspaceTable{
OwnerID: user.ID,
TemplateID: template.ID,
OrganizationID: templateJob.OrganizationID,
LastUsedAt: dbtime.Now(),
Ttl: sql.NullInt64{Valid: true, Int64: int64(48 * time.Hour)},
})
// Ensure the workspace start with the correct TTLs
require.Equal(t, sql.NullInt64{Valid: true, Int64: int64(48 * time.Hour)}, workspace.Ttl)
// Disable AllowUserAutostop
template, err = templateScheduleStore.Set(ctx, db, template, agplschedule.TemplateScheduleOptions{
DefaultTTL: 23 * time.Hour,
UserAutostopEnabled: false,
})
require.NoError(t, err)
// Ensure the workspace ends with the correct TTLs
ws, err := db.GetWorkspaceByID(ctx, workspace.ID)
require.NoError(t, err)
require.Equal(t, sql.NullInt64{Valid: true, Int64: int64(23 * time.Hour)}, ws.Ttl)
})
}
func must[V any](v V, err error) V {
if err != nil {
panic(err)