mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 [ ])
|
||||
`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user