Skip to content

Commit 40624bf

Browse files
fix: update workspace TTL on template TTL change (coder#15761)
Relates to coder#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.
1 parent 67553a7 commit 40624bf

File tree

12 files changed

+518
-0
lines changed

12 files changed

+518
-0
lines changed

coderd/database/dbauthz/dbauthz.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4138,6 +4138,17 @@ func (q *querier) UpdateWorkspacesDormantDeletingAtByTemplateID(ctx context.Cont
41384138
return q.db.UpdateWorkspacesDormantDeletingAtByTemplateID(ctx, arg)
41394139
}
41404140

4141+
func (q *querier) UpdateWorkspacesTTLByTemplateID(ctx context.Context, arg database.UpdateWorkspacesTTLByTemplateIDParams) error {
4142+
template, err := q.db.GetTemplateByID(ctx, arg.TemplateID)
4143+
if err != nil {
4144+
return xerrors.Errorf("get template by id: %w", err)
4145+
}
4146+
if err := q.authorizeContext(ctx, policy.ActionUpdate, template); err != nil {
4147+
return err
4148+
}
4149+
return q.db.UpdateWorkspacesTTLByTemplateID(ctx, arg)
4150+
}
4151+
41414152
func (q *querier) UpsertAnnouncementBanners(ctx context.Context, value string) error {
41424153
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceDeploymentConfig); err != nil {
41434154
return err

coderd/database/dbauthz/dbauthz_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1017,6 +1017,12 @@ func (s *MethodTestSuite) TestTemplate() {
10171017
TemplateID: t1.ID,
10181018
}).Asserts(t1, policy.ActionUpdate)
10191019
}))
1020+
s.Run("UpdateWorkspacesTTLByTemplateID", s.Subtest(func(db database.Store, check *expects) {
1021+
t1 := dbgen.Template(s.T(), db, database.Template{})
1022+
check.Args(database.UpdateWorkspacesTTLByTemplateIDParams{
1023+
TemplateID: t1.ID,
1024+
}).Asserts(t1, policy.ActionUpdate)
1025+
}))
10201026
s.Run("UpdateTemplateActiveVersionByID", s.Subtest(func(db database.Store, check *expects) {
10211027
t1 := dbgen.Template(s.T(), db, database.Template{
10221028
ActiveVersionID: uuid.New(),

coderd/database/dbmem/dbmem.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10192,6 +10192,26 @@ func (q *FakeQuerier) UpdateWorkspacesDormantDeletingAtByTemplateID(_ context.Co
1019210192
return affectedRows, nil
1019310193
}
1019410194

10195+
func (q *FakeQuerier) UpdateWorkspacesTTLByTemplateID(_ context.Context, arg database.UpdateWorkspacesTTLByTemplateIDParams) error {
10196+
err := validateDatabaseType(arg)
10197+
if err != nil {
10198+
return err
10199+
}
10200+
10201+
q.mutex.Lock()
10202+
defer q.mutex.Unlock()
10203+
10204+
for i, ws := range q.workspaces {
10205+
if ws.TemplateID != arg.TemplateID {
10206+
continue
10207+
}
10208+
10209+
q.workspaces[i].Ttl = arg.Ttl
10210+
}
10211+
10212+
return nil
10213+
}
10214+
1019510215
func (q *FakeQuerier) UpsertAnnouncementBanners(_ context.Context, data string) error {
1019610216
q.mutex.RLock()
1019710217
defer q.mutex.RUnlock()

coderd/database/dbmetrics/querymetrics.go

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/dbmock/dbmock.go

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/querier.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries.sql.go

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries/workspaces.sql

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,14 @@ SET
501501
WHERE
502502
id = $1;
503503

504+
-- name: UpdateWorkspacesTTLByTemplateID :exec
505+
UPDATE
506+
workspaces
507+
SET
508+
ttl = $2
509+
WHERE
510+
template_id = $1;
511+
504512
-- name: UpdateWorkspaceLastUsedAt :exec
505513
UPDATE
506514
workspaces

coderd/schedule/template.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package schedule
22

33
import (
44
"context"
5+
"database/sql"
56
"time"
67

78
"github.com/google/uuid"
@@ -228,6 +229,23 @@ func (*agplTemplateScheduleStore) Set(ctx context.Context, db database.Store, tp
228229
return xerrors.Errorf("update template schedule: %w", err)
229230
}
230231

232+
// Users running the AGPL version are unable to customize their workspaces
233+
// autostop, so we want to keep their workspaces in track with any template
234+
// TTL changes.
235+
if tpl.DefaultTTL != int64(opts.DefaultTTL) {
236+
var ttl sql.NullInt64
237+
if opts.DefaultTTL != 0 {
238+
ttl = sql.NullInt64{Valid: true, Int64: int64(opts.DefaultTTL)}
239+
}
240+
241+
if err = db.UpdateWorkspacesTTLByTemplateID(ctx, database.UpdateWorkspacesTTLByTemplateIDParams{
242+
TemplateID: tpl.ID,
243+
Ttl: ttl,
244+
}); err != nil {
245+
return xerrors.Errorf("update workspace ttl by template id %q: %w", tpl.ID, err)
246+
}
247+
}
248+
231249
template, err = db.GetTemplateByID(ctx, tpl.ID)
232250
if err != nil {
233251
return xerrors.Errorf("fetch updated template: %w", err)

coderd/schedule/template_test.go

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package schedule_test
2+
3+
import (
4+
"database/sql"
5+
"testing"
6+
"time"
7+
8+
"github.com/stretchr/testify/require"
9+
10+
"github.com/coder/coder/v2/coderd/database"
11+
"github.com/coder/coder/v2/coderd/database/dbgen"
12+
"github.com/coder/coder/v2/coderd/database/dbtestutil"
13+
"github.com/coder/coder/v2/coderd/database/dbtime"
14+
"github.com/coder/coder/v2/coderd/schedule"
15+
"github.com/coder/coder/v2/testutil"
16+
)
17+
18+
func TestTemplateTTL(t *testing.T) {
19+
t.Parallel()
20+
21+
tests := []struct {
22+
name string
23+
fromTTL time.Duration
24+
toTTL time.Duration
25+
expected sql.NullInt64
26+
}{
27+
{
28+
name: "ModifyTTLDurationDown",
29+
fromTTL: 24 * time.Hour,
30+
toTTL: 1 * time.Hour,
31+
expected: sql.NullInt64{Valid: true, Int64: int64(1 * time.Hour)},
32+
},
33+
{
34+
name: "ModifyTTLDurationUp",
35+
fromTTL: 24 * time.Hour,
36+
toTTL: 36 * time.Hour,
37+
expected: sql.NullInt64{Valid: true, Int64: int64(36 * time.Hour)},
38+
},
39+
{
40+
name: "ModifyTTLDurationSame",
41+
fromTTL: 24 * time.Hour,
42+
toTTL: 24 * time.Hour,
43+
expected: sql.NullInt64{Valid: true, Int64: int64(24 * time.Hour)},
44+
},
45+
{
46+
name: "DisableTTL",
47+
fromTTL: 24 * time.Hour,
48+
toTTL: 0,
49+
expected: sql.NullInt64{},
50+
},
51+
}
52+
53+
for _, tt := range tests {
54+
tt := tt
55+
56+
t.Run(tt.name, func(t *testing.T) {
57+
t.Parallel()
58+
59+
var (
60+
db, _ = dbtestutil.NewDB(t)
61+
ctx = testutil.Context(t, testutil.WaitLong)
62+
user = dbgen.User(t, db, database.User{})
63+
file = dbgen.File(t, db, database.File{CreatedBy: user.ID})
64+
// Create first template
65+
templateJob = dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
66+
FileID: file.ID,
67+
InitiatorID: user.ID,
68+
Tags: database.StringMap{"foo": "bar"},
69+
})
70+
templateVersion = dbgen.TemplateVersion(t, db, database.TemplateVersion{
71+
CreatedBy: user.ID,
72+
JobID: templateJob.ID,
73+
OrganizationID: templateJob.OrganizationID,
74+
})
75+
template = dbgen.Template(t, db, database.Template{
76+
ActiveVersionID: templateVersion.ID,
77+
CreatedBy: user.ID,
78+
OrganizationID: templateJob.OrganizationID,
79+
})
80+
// Create second template
81+
otherTTL = tt.fromTTL + 6*time.Hour
82+
otherTemplateJob = dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
83+
FileID: file.ID,
84+
InitiatorID: user.ID,
85+
Tags: database.StringMap{"foo": "bar"},
86+
})
87+
otherTemplateVersion = dbgen.TemplateVersion(t, db, database.TemplateVersion{
88+
CreatedBy: user.ID,
89+
JobID: otherTemplateJob.ID,
90+
OrganizationID: otherTemplateJob.OrganizationID,
91+
})
92+
otherTemplate = dbgen.Template(t, db, database.Template{
93+
ActiveVersionID: otherTemplateVersion.ID,
94+
CreatedBy: user.ID,
95+
OrganizationID: otherTemplateJob.OrganizationID,
96+
})
97+
)
98+
99+
templateScheduleStore := schedule.NewAGPLTemplateScheduleStore()
100+
101+
// Set both template's default TTL
102+
template, err := templateScheduleStore.Set(ctx, db, template, schedule.TemplateScheduleOptions{
103+
DefaultTTL: tt.fromTTL,
104+
})
105+
require.NoError(t, err)
106+
otherTemplate, err = templateScheduleStore.Set(ctx, db, otherTemplate, schedule.TemplateScheduleOptions{
107+
DefaultTTL: otherTTL,
108+
})
109+
require.NoError(t, err)
110+
111+
// We create two workspaces here, one with the template we're modifying, the
112+
// other with a different template. We want to ensure we only modify one
113+
// of the workspaces.
114+
workspace := dbgen.Workspace(t, db, database.WorkspaceTable{
115+
OwnerID: user.ID,
116+
TemplateID: template.ID,
117+
OrganizationID: templateJob.OrganizationID,
118+
LastUsedAt: dbtime.Now(),
119+
Ttl: sql.NullInt64{Valid: true, Int64: int64(tt.fromTTL)},
120+
})
121+
otherWorkspace := dbgen.Workspace(t, db, database.WorkspaceTable{
122+
OwnerID: user.ID,
123+
TemplateID: otherTemplate.ID,
124+
OrganizationID: otherTemplateJob.OrganizationID,
125+
LastUsedAt: dbtime.Now(),
126+
Ttl: sql.NullInt64{Valid: true, Int64: int64(otherTTL)},
127+
})
128+
129+
// Ensure the workspace's start with the correct TTLs
130+
require.Equal(t, sql.NullInt64{Valid: true, Int64: int64(tt.fromTTL)}, workspace.Ttl)
131+
require.Equal(t, sql.NullInt64{Valid: true, Int64: int64(otherTTL)}, otherWorkspace.Ttl)
132+
133+
// Update _only_ the primary template's TTL
134+
_, err = templateScheduleStore.Set(ctx, db, template, schedule.TemplateScheduleOptions{
135+
DefaultTTL: tt.toTTL,
136+
})
137+
require.NoError(t, err)
138+
139+
// Verify the primary workspace's TTL has been updated.
140+
ws, err := db.GetWorkspaceByID(ctx, workspace.ID)
141+
require.NoError(t, err)
142+
require.Equal(t, tt.expected, ws.Ttl)
143+
144+
// Verify that the other workspace's TTL has not been touched.
145+
ws, err = db.GetWorkspaceByID(ctx, otherWorkspace.ID)
146+
require.NoError(t, err)
147+
require.Equal(t, sql.NullInt64{Valid: true, Int64: int64(otherTTL)}, ws.Ttl)
148+
})
149+
}
150+
}

enterprise/coderd/schedule/template.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,23 @@ func (s *EnterpriseTemplateScheduleStore) Set(ctx context.Context, db database.S
195195
return xerrors.Errorf("get updated template schedule: %w", err)
196196
}
197197

198+
// Update all workspace's TTL using this template if either of the following:
199+
// - The template's AllowUserAutostop has just been disabled
200+
// - The template's TTL has been modified and AllowUserAutostop is disabled
201+
if !opts.UserAutostopEnabled && (tpl.AllowUserAutostop || int64(opts.DefaultTTL) != tpl.DefaultTTL) {
202+
var ttl sql.NullInt64
203+
if opts.DefaultTTL != 0 {
204+
ttl = sql.NullInt64{Valid: true, Int64: int64(opts.DefaultTTL)}
205+
}
206+
207+
if err = tx.UpdateWorkspacesTTLByTemplateID(ctx, database.UpdateWorkspacesTTLByTemplateIDParams{
208+
TemplateID: template.ID,
209+
Ttl: ttl,
210+
}); err != nil {
211+
return xerrors.Errorf("update workspaces ttl by template id %q: %w", template.ID, err)
212+
}
213+
}
214+
198215
// Recalculate max_deadline and deadline for all running workspace
199216
// builds on this template.
200217
err = s.updateWorkspaceBuilds(ctx, tx, template)

0 commit comments

Comments
 (0)