From f99d45d25754a932a1d4e24a9ec93fc3424935b1 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Wed, 4 Dec 2024 16:11:50 +0000 Subject: [PATCH 1/6] fix: update workspace TTL on template TTL change --- coderd/database/dbauthz/dbauthz.go | 11 ++ coderd/database/dbauthz/dbauthz_test.go | 6 + coderd/database/dbmem/dbmem.go | 20 +++ coderd/database/dbmetrics/querymetrics.go | 7 ++ coderd/database/dbmock/dbmock.go | 14 +++ coderd/database/querier.go | 1 + coderd/database/queries.sql.go | 19 +++ coderd/database/queries/workspaces.sql | 8 ++ coderd/schedule/template.go | 18 +++ coderd/schedule/template_test.go | 73 +++++++++++ enterprise/coderd/schedule/template.go | 16 +++ enterprise/coderd/schedule/template_test.go | 131 ++++++++++++++++++++ 12 files changed, 324 insertions(+) create mode 100644 coderd/schedule/template_test.go diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 21514cdf4e590..2d5e440575cab 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -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 diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index e97523c918fc3..5c8cb7766a960 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -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(), diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 9196b6bcb94eb..c5b9f7f6cc06f 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -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() diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go index de43d05724467..797e3648aaff6 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -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) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 5c1c401046169..9d05156496580 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -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() diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 5593b9a14beb6..371dde97034db 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -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 diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 96fe9e5f8bf9a..ee2b71a4a6c63 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -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 [ ]) ` diff --git a/coderd/database/queries/workspaces.sql b/coderd/database/queries/workspaces.sql index cdf4dfa5f0e3e..cb0d11e8a8960 100644 --- a/coderd/database/queries/workspaces.sql +++ b/coderd/database/queries/workspaces.sql @@ -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 diff --git a/coderd/schedule/template.go b/coderd/schedule/template.go index a68cebd1fac93..ac7106af88173 100644 --- a/coderd/schedule/template.go +++ b/coderd/schedule/template.go @@ -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) diff --git a/coderd/schedule/template_test.go b/coderd/schedule/template_test.go new file mode 100644 index 0000000000000..38ec7fe87fc50 --- /dev/null +++ b/coderd/schedule/template_test.go @@ -0,0 +1,73 @@ +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() + + t.Run("ModifiesWorkspaceTTL", 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}) + templateJob = dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{ + FileID: file.ID, + InitiatorID: user.ID, + Tags: database.StringMap{"foo": "bar"}, + }) + defaultTTL = 24 * time.Hour + 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, + DefaultTTL: int64(defaultTTL), + }) + 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(defaultTTL)}, + }) + ) + + templateScheduleStore := schedule.NewAGPLTemplateScheduleStore() + + // We've created a template with a TTL of 24 hours, so we expect our + // workspace to have a TTL of 24 hours. + require.Equal(t, sql.NullInt64{Valid: true, Int64: int64(defaultTTL)}, workspace.Ttl) + + // We expect an AGPL template schedule store to always update + // the TTL of existing workspaces. + _, err := templateScheduleStore.Set(ctx, db, template, schedule.TemplateScheduleOptions{ + DefaultTTL: 1 * time.Hour, + }) + require.NoError(t, err) + + // Verify that the workspace's TTL has been updated. + ws, err := db.GetWorkspaceByID(ctx, workspace.ID) + require.NoError(t, err) + require.Equal(t, sql.NullInt64{Valid: true, Int64: int64(1 * time.Hour)}, ws.Ttl) + }) +} diff --git a/enterprise/coderd/schedule/template.go b/enterprise/coderd/schedule/template.go index 82ec97b531a5a..18696239652c0 100644 --- a/enterprise/coderd/schedule/template.go +++ b/enterprise/coderd/schedule/template.go @@ -195,6 +195,22 @@ func (s *EnterpriseTemplateScheduleStore) Set(ctx context.Context, db database.S return xerrors.Errorf("get updated template schedule: %w", err) } + // If this template disallows users from customizing their own autostop, then + // we want to keep their workspaces in track with any template TTL changes. + if !template.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) diff --git a/enterprise/coderd/schedule/template_test.go b/enterprise/coderd/schedule/template_test.go index 5e3c9fd658cf3..dc10fc60eb77c 100644 --- a/enterprise/coderd/schedule/template_test.go +++ b/enterprise/coderd/schedule/template_test.go @@ -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,136 @@ func TestNotifications(t *testing.T) { }) } +func TestTemplateTTL(t *testing.T) { + t.Parallel() + + t.Run("ModifiesWorkspaceTTLWhenAllowUserAutostopIsFalse", 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}) + templateJob = dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{ + FileID: file.ID, + InitiatorID: user.ID, + Tags: database.StringMap{"foo": "bar"}, + }) + defaultTTL = 24 * time.Hour + 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, + DefaultTTL: int64(defaultTTL), + }) + 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(defaultTTL)}, + }) + ) + + // 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) + + // We've created a template with a TTL of 24 hours, so we expect our + // workspace to have a TTL of 24 hours. + require.Equal(t, sql.NullInt64{Valid: true, Int64: int64(defaultTTL)}, workspace.Ttl) + + // As our template has `AllowUserAutostop` false, we expect that modifying + // the template's TTL should modify any workspaces that use this template. + _, err = templateScheduleStore.Set(ctx, db, template, agplschedule.TemplateScheduleOptions{ + UserAutostopEnabled: false, + DefaultTTL: 1 * time.Hour, + }) + require.NoError(t, err) + + // Verify that the workspace's TTL time has been updated. + ws, err := db.GetWorkspaceByID(ctx, workspace.ID) + require.NoError(t, err) + require.Equal(t, sql.NullInt64{Valid: true, Int64: int64(1 * time.Hour)}, ws.Ttl) + }) + + t.Run("DoesNotModifyWorkspaceTTLWhenAllowUserAutostopIsTrue", 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}) + templateJob = dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{ + FileID: file.ID, + InitiatorID: user.ID, + Tags: database.StringMap{"foo": "bar"}, + }) + defaultTTL = 24 * time.Hour + 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: true, + DefaultTTL: int64(defaultTTL), + }) + 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(defaultTTL)}, + }) + ) + + // 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) + + // We've created a template with a TTL of 24 hours, so we expect our + // workspace to have a TTL of 24 hours. + require.Equal(t, sql.NullInt64{Valid: true, Int64: int64(defaultTTL)}, workspace.Ttl) + + // As our template has `AllowUserAutostop` true, we expect that modifying + // the template's TTL should NOT modify any workspaces that use this template. + _, err = templateScheduleStore.Set(ctx, db, template, agplschedule.TemplateScheduleOptions{ + UserAutostopEnabled: true, + DefaultTTL: 1 * time.Hour, + }) + require.NoError(t, err) + + // Verify that the workspace's TTL not been updated. + ws, err := db.GetWorkspaceByID(ctx, workspace.ID) + require.NoError(t, err) + require.Equal(t, sql.NullInt64{Valid: true, Int64: int64(defaultTTL)}, ws.Ttl) + }) +} + func must[V any](v V, err error) V { if err != nil { panic(err) From 3b88720f2bb39295cfd222089cda622bcd5cf906 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Thu, 5 Dec 2024 10:06:18 +0000 Subject: [PATCH 2/6] tests: refactor --- coderd/schedule/template_test.go | 151 ++++++++---- enterprise/coderd/schedule/template_test.go | 252 +++++++++++--------- 2 files changed, 254 insertions(+), 149 deletions(-) diff --git a/coderd/schedule/template_test.go b/coderd/schedule/template_test.go index 38ec7fe87fc50..01de4c79d282c 100644 --- a/coderd/schedule/template_test.go +++ b/coderd/schedule/template_test.go @@ -18,56 +18,127 @@ import ( func TestTemplateTTL(t *testing.T) { t.Parallel() - t.Run("ModifiesWorkspaceTTL", func(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: "DisableTTL", + fromTTL: 24 * time.Hour, + toTTL: 0, + expected: sql.NullInt64{}, + }, + } - 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}) - templateJob = dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{ - FileID: file.ID, - InitiatorID: user.ID, - Tags: database.StringMap{"foo": "bar"}, - }) - defaultTTL = 24 * time.Hour - templateVersion = dbgen.TemplateVersion(t, db, database.TemplateVersion{ - CreatedBy: user.ID, - JobID: templateJob.ID, - OrganizationID: templateJob.OrganizationID, + 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, }) - template = dbgen.Template(t, db, database.Template{ - ActiveVersionID: templateVersion.ID, - CreatedBy: user.ID, - OrganizationID: templateJob.OrganizationID, - DefaultTTL: int64(defaultTTL), + require.NoError(t, err) + otherTemplate, err = templateScheduleStore.Set(ctx, db, otherTemplate, schedule.TemplateScheduleOptions{ + DefaultTTL: otherTTL, }) - workspace = dbgen.Workspace(t, db, database.WorkspaceTable{ + 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(defaultTTL)}, + 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)}, }) - ) - templateScheduleStore := schedule.NewAGPLTemplateScheduleStore() + // 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) - // We've created a template with a TTL of 24 hours, so we expect our - // workspace to have a TTL of 24 hours. - require.Equal(t, sql.NullInt64{Valid: true, Int64: int64(defaultTTL)}, workspace.Ttl) + // Update _only_ the primary template's TTL + _, err = templateScheduleStore.Set(ctx, db, template, schedule.TemplateScheduleOptions{ + DefaultTTL: tt.toTTL, + }) + require.NoError(t, err) - // We expect an AGPL template schedule store to always update - // the TTL of existing workspaces. - _, err := templateScheduleStore.Set(ctx, db, template, schedule.TemplateScheduleOptions{ - DefaultTTL: 1 * time.Hour, - }) - 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 workspace's TTL has been updated. - ws, err := db.GetWorkspaceByID(ctx, workspace.ID) - require.NoError(t, err) - require.Equal(t, sql.NullInt64{Valid: true, Int64: int64(1 * time.Hour)}, 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) + }) + } } diff --git a/enterprise/coderd/schedule/template_test.go b/enterprise/coderd/schedule/template_test.go index dc10fc60eb77c..46711c5838c01 100644 --- a/enterprise/coderd/schedule/template_test.go +++ b/enterprise/coderd/schedule/template_test.go @@ -712,131 +712,165 @@ func TestNotifications(t *testing.T) { func TestTemplateTTL(t *testing.T) { t.Parallel() - t.Run("ModifiesWorkspaceTTLWhenAllowUserAutostopIsFalse", 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}) - templateJob = dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{ - FileID: file.ID, - InitiatorID: user.ID, - Tags: database.StringMap{"foo": "bar"}, - }) - defaultTTL = 24 * time.Hour - 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, - DefaultTTL: int64(defaultTTL), - }) - 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(defaultTTL)}, - }) - ) - - // 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) + 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/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/DisableTTL", + allowUserAutostop: true, + fromTTL: 24 * time.Hour, + toTTL: 0, + expected: sql.NullInt64{Valid: true, Int64: int64(24 * time.Hour)}, + }, + } - // We've created a template with a TTL of 24 hours, so we expect our - // workspace to have a TTL of 24 hours. - require.Equal(t, sql.NullInt64{Valid: true, Int64: int64(defaultTTL)}, workspace.Ttl) + for _, tt := range tests { + tt := tt - // As our template has `AllowUserAutostop` false, we expect that modifying - // the template's TTL should modify any workspaces that use this template. - _, err = templateScheduleStore.Set(ctx, db, template, agplschedule.TemplateScheduleOptions{ - UserAutostopEnabled: false, - DefaultTTL: 1 * time.Hour, - }) - require.NoError(t, err) + t.Run(tt.name, func(t *testing.T) { + t.Parallel() - // Verify that the workspace's TTL time has been updated. - ws, err := db.GetWorkspaceByID(ctx, workspace.ID) - require.NoError(t, err) - require.Equal(t, sql.NullInt64{Valid: true, Int64: int64(1 * time.Hour)}, ws.Ttl) - }) + 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, + }) + ) - t.Run("DoesNotModifyWorkspaceTTLWhenAllowUserAutostopIsTrue", func(t *testing.T) { - t.Parallel() + // 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) - 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}) - templateJob = dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{ - FileID: file.ID, - InitiatorID: user.ID, - Tags: database.StringMap{"foo": "bar"}, + // Set both template's default TTL + template, err = templateScheduleStore.Set(ctx, db, template, agplschedule.TemplateScheduleOptions{ + DefaultTTL: tt.fromTTL, }) - defaultTTL = 24 * time.Hour - 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: true, - DefaultTTL: int64(defaultTTL), + require.NoError(t, err) + otherTemplate, err = templateScheduleStore.Set(ctx, db, otherTemplate, agplschedule.TemplateScheduleOptions{ + DefaultTTL: otherTTL, }) - workspace = dbgen.Workspace(t, db, database.WorkspaceTable{ + 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(defaultTTL)}, + 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)}, }) - ) - // 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) + // 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) - // We've created a template with a TTL of 24 hours, so we expect our - // workspace to have a TTL of 24 hours. - require.Equal(t, sql.NullInt64{Valid: true, Int64: int64(defaultTTL)}, workspace.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) - // As our template has `AllowUserAutostop` true, we expect that modifying - // the template's TTL should NOT modify any workspaces that use this template. - _, err = templateScheduleStore.Set(ctx, db, template, agplschedule.TemplateScheduleOptions{ - UserAutostopEnabled: true, - DefaultTTL: 1 * time.Hour, - }) - 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 that the workspace's TTL not been updated. - ws, err := db.GetWorkspaceByID(ctx, workspace.ID) - require.NoError(t, err) - require.Equal(t, sql.NullInt64{Valid: true, Int64: int64(defaultTTL)}, 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) + }) + } } func must[V any](v V, err error) V { From 021a8066bc18b3c6528e1ce871a60d6499ae0236 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Thu, 5 Dec 2024 10:15:46 +0000 Subject: [PATCH 3/6] fix: parameters --- enterprise/coderd/schedule/template_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enterprise/coderd/schedule/template_test.go b/enterprise/coderd/schedule/template_test.go index 46711c5838c01..f44420c991968 100644 --- a/enterprise/coderd/schedule/template_test.go +++ b/enterprise/coderd/schedule/template_test.go @@ -819,7 +819,7 @@ func TestTemplateTTL(t *testing.T) { require.NoError(t, err) userQuietHoursStorePtr := &atomic.Pointer[agplschedule.UserQuietHoursScheduleStore]{} userQuietHoursStorePtr.Store(&userQuietHoursStore) - templateScheduleStore := schedule.NewEnterpriseTemplateScheduleStore(userQuietHoursStorePtr, notifyEnq, logger) + templateScheduleStore := schedule.NewEnterpriseTemplateScheduleStore(userQuietHoursStorePtr, notifyEnq, logger, nil) // Set both template's default TTL template, err = templateScheduleStore.Set(ctx, db, template, agplschedule.TemplateScheduleOptions{ From dca1bc70c29084a709f6a11728f56b5f6ac1b357 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Thu, 5 Dec 2024 11:32:53 +0000 Subject: [PATCH 4/6] fix: set a workspace's ttl to template's ttl if allow user autostop gets disabled --- enterprise/coderd/schedule/template.go | 8 ++- enterprise/coderd/schedule/template_test.go | 68 +++++++++++++++++++++ 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/enterprise/coderd/schedule/template.go b/enterprise/coderd/schedule/template.go index 18696239652c0..709e55a13bf38 100644 --- a/enterprise/coderd/schedule/template.go +++ b/enterprise/coderd/schedule/template.go @@ -195,9 +195,11 @@ func (s *EnterpriseTemplateScheduleStore) Set(ctx context.Context, db database.S return xerrors.Errorf("get updated template schedule: %w", err) } - // If this template disallows users from customizing their own autostop, then - // we want to keep their workspaces in track with any template TTL changes. - if !template.AllowUserAutostop && int64(opts.DefaultTTL) != tpl.DefaultTTL { + // 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 && opts.UserAutostopEnabled != tpl.AllowUserAutostop) || + (!template.AllowUserAutostop && int64(opts.DefaultTTL) != tpl.DefaultTTL) { var ttl sql.NullInt64 if opts.DefaultTTL != 0 { ttl = sql.NullInt64{Valid: true, Int64: int64(opts.DefaultTTL)} diff --git a/enterprise/coderd/schedule/template_test.go b/enterprise/coderd/schedule/template_test.go index f44420c991968..94519b8415b8b 100644 --- a/enterprise/coderd/schedule/template_test.go +++ b/enterprise/coderd/schedule/template_test.go @@ -871,6 +871,74 @@ func TestTemplateTTL(t *testing.T) { 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 to the template schedule + 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's 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: 24 * time.Hour, + UserAutostopEnabled: false, + }) + require.NoError(t, err) + + // Ensure the workspace's 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(24 * time.Hour)}, ws.Ttl) + }) } func must[V any](v V, err error) V { From d1cb4d0ade08f8c1433b04177047ad7e356c52aa Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Thu, 5 Dec 2024 11:36:47 +0000 Subject: [PATCH 5/6] chore: typos --- enterprise/coderd/schedule/template_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/enterprise/coderd/schedule/template_test.go b/enterprise/coderd/schedule/template_test.go index 94519b8415b8b..e2edbd3e3a413 100644 --- a/enterprise/coderd/schedule/template_test.go +++ b/enterprise/coderd/schedule/template_test.go @@ -915,7 +915,7 @@ func TestTemplateTTL(t *testing.T) { }) require.NoError(t, err) - // Create a workspace with a TTL different to the template schedule + // 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, @@ -924,7 +924,7 @@ func TestTemplateTTL(t *testing.T) { Ttl: sql.NullInt64{Valid: true, Int64: int64(48 * time.Hour)}, }) - // Ensure the workspace's start with the correct TTLs + // Ensure the workspace start with the correct TTLs require.Equal(t, sql.NullInt64{Valid: true, Int64: int64(48 * time.Hour)}, workspace.Ttl) // Disable AllowUserAutostop @@ -934,7 +934,7 @@ func TestTemplateTTL(t *testing.T) { }) require.NoError(t, err) - // Ensure the workspace's ends with the correct TTLs + // 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(24 * time.Hour)}, ws.Ttl) From 66e80cf5a503740ef51fd1140acbef020545e86a Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Fri, 6 Dec 2024 09:23:59 +0000 Subject: [PATCH 6/6] chore: changes from feedback --- coderd/schedule/template_test.go | 6 ++++++ enterprise/coderd/schedule/template.go | 3 +-- enterprise/coderd/schedule/template_test.go | 18 ++++++++++++++++-- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/coderd/schedule/template_test.go b/coderd/schedule/template_test.go index 01de4c79d282c..7de7caa05c10f 100644 --- a/coderd/schedule/template_test.go +++ b/coderd/schedule/template_test.go @@ -36,6 +36,12 @@ func TestTemplateTTL(t *testing.T) { 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, diff --git a/enterprise/coderd/schedule/template.go b/enterprise/coderd/schedule/template.go index 709e55a13bf38..b1065aee7d2b6 100644 --- a/enterprise/coderd/schedule/template.go +++ b/enterprise/coderd/schedule/template.go @@ -198,8 +198,7 @@ func (s *EnterpriseTemplateScheduleStore) Set(ctx context.Context, db database.S // 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 && opts.UserAutostopEnabled != tpl.AllowUserAutostop) || - (!template.AllowUserAutostop && int64(opts.DefaultTTL) != tpl.DefaultTTL) { + 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)} diff --git a/enterprise/coderd/schedule/template_test.go b/enterprise/coderd/schedule/template_test.go index e2edbd3e3a413..712fa032c8c1b 100644 --- a/enterprise/coderd/schedule/template_test.go +++ b/enterprise/coderd/schedule/template_test.go @@ -733,6 +733,13 @@ func TestTemplateTTL(t *testing.T) { 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, @@ -754,6 +761,13 @@ func TestTemplateTTL(t *testing.T) { 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, @@ -929,7 +943,7 @@ func TestTemplateTTL(t *testing.T) { // Disable AllowUserAutostop template, err = templateScheduleStore.Set(ctx, db, template, agplschedule.TemplateScheduleOptions{ - DefaultTTL: 24 * time.Hour, + DefaultTTL: 23 * time.Hour, UserAutostopEnabled: false, }) require.NoError(t, err) @@ -937,7 +951,7 @@ func TestTemplateTTL(t *testing.T) { // 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(24 * time.Hour)}, ws.Ttl) + require.Equal(t, sql.NullInt64{Valid: true, Int64: int64(23 * time.Hour)}, ws.Ttl) }) } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy