Skip to content

Commit c2b05f0

Browse files
committed
feat: implement deprecated flag for templates to prevent new workspaces
1 parent 9f3a955 commit c2b05f0

16 files changed

+205
-17
lines changed

cli/templateedit.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
)
1717

1818
func (r *RootCmd) templateEdit() *clibase.Cmd {
19+
const deprecatedFlagName = "deprecated"
1920
var (
2021
name string
2122
displayName string
@@ -32,6 +33,7 @@ func (r *RootCmd) templateEdit() *clibase.Cmd {
3233
allowUserAutostart bool
3334
allowUserAutostop bool
3435
requireActiveVersion bool
36+
deprecatedMessage string
3537
)
3638
client := new(codersdk.Client)
3739

@@ -118,6 +120,15 @@ func (r *RootCmd) templateEdit() *clibase.Cmd {
118120
autostopRequirementDaysOfWeek = []string{}
119121
}
120122

123+
// Only pass explicitly set deprecated values since the empty string
124+
// removes the deprecated message. By default if we pass a nil,
125+
// there is no change to this field.
126+
var deprecated *string
127+
opt := inv.Command.Options.ByName(deprecatedFlagName)
128+
if !(opt.ValueSource == "" || opt.ValueSource == clibase.ValueSourceDefault) {
129+
deprecated = &deprecatedMessage
130+
}
131+
121132
// NOTE: coderd will ignore empty fields.
122133
req := codersdk.UpdateTemplateMeta{
123134
Name: name,
@@ -139,6 +150,7 @@ func (r *RootCmd) templateEdit() *clibase.Cmd {
139150
AllowUserAutostart: allowUserAutostart,
140151
AllowUserAutostop: allowUserAutostop,
141152
RequireActiveVersion: requireActiveVersion,
153+
DeprecatedMessage: deprecated,
142154
}
143155

144156
_, err = client.UpdateTemplateMeta(inv.Context(), template.ID, req)
@@ -166,6 +178,12 @@ func (r *RootCmd) templateEdit() *clibase.Cmd {
166178
Description: "Edit the template description.",
167179
Value: clibase.StringOf(&description),
168180
},
181+
{
182+
Name: deprecatedFlagName,
183+
Flag: "deprecated",
184+
Description: "Sets the template as deprecated. Must be a message explaining why the template is deprecated.",
185+
Value: clibase.StringOf(&deprecatedMessage),
186+
},
169187
{
170188
Flag: "icon",
171189
Description: "Edit the template icon path.",

coderd/database/dbauthz/accesscontrol.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ type AccessControlStore interface {
1818

1919
type TemplateAccessControl struct {
2020
RequireActiveVersion bool
21+
Deprecated string
22+
}
23+
24+
func (t TemplateAccessControl) IsDeprecated() bool {
25+
return t.Deprecated != ""
2126
}
2227

2328
// AGPLTemplateAccessControlStore always returns the defaults for access control
@@ -26,9 +31,14 @@ type AGPLTemplateAccessControlStore struct{}
2631

2732
var _ AccessControlStore = AGPLTemplateAccessControlStore{}
2833

29-
func (AGPLTemplateAccessControlStore) GetTemplateAccessControl(database.Template) TemplateAccessControl {
34+
func (AGPLTemplateAccessControlStore) GetTemplateAccessControl(t database.Template) TemplateAccessControl {
3035
return TemplateAccessControl{
3136
RequireActiveVersion: false,
37+
// AGPL cannot set deprecated templates, but it should return
38+
// existing deprecated templates. This is erroring on the safe side
39+
// if a license expires, we should not allow deprecated templates
40+
// to be used for new workspaces.
41+
Deprecated: t.Deprecated,
3242
}
3343
}
3444

coderd/database/dbmem/dbmem.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5905,6 +5905,7 @@ func (q *FakeQuerier) UpdateTemplateAccessControlByID(_ context.Context, arg dat
59055905
continue
59065906
}
59075907
q.templates[idx].RequireActiveVersion = arg.RequireActiveVersion
5908+
q.templates[idx].Deprecated = arg.Deprecated
59085909
return nil
59095910
}
59105911

coderd/database/dump.sql

Lines changed: 5 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
BEGIN;
2+
3+
DROP VIEW template_with_users;
4+
5+
ALTER TABLE templates
6+
DROP COLUMN deprecated;
7+
8+
CREATE VIEW
9+
template_with_users
10+
AS
11+
SELECT
12+
templates.*,
13+
coalesce(visible_users.avatar_url, '') AS created_by_avatar_url,
14+
coalesce(visible_users.username, '') AS created_by_username
15+
FROM
16+
templates
17+
LEFT JOIN
18+
visible_users
19+
ON
20+
templates.created_by = visible_users.id;
21+
22+
COMMENT ON VIEW template_with_users IS 'Joins in the username + avatar url of the created by user.';
23+
24+
COMMIT;
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
BEGIN;
2+
3+
-- The view will be rebuilt with the new column
4+
DROP VIEW template_with_users;
5+
6+
ALTER TABLE templates
7+
ADD COLUMN deprecated TEXT NOT NULL DEFAULT '';
8+
9+
COMMENT ON COLUMN templates.deprecated IS 'If set to a non empty string, the template will no longer be able to be used. The message will be displayed to the user.';
10+
11+
-- Restore the old version of the template_with_users view.
12+
CREATE VIEW
13+
template_with_users
14+
AS
15+
SELECT
16+
templates.*,
17+
coalesce(visible_users.avatar_url, '') AS created_by_avatar_url,
18+
coalesce(visible_users.username, '') AS created_by_username
19+
FROM
20+
templates
21+
LEFT JOIN
22+
visible_users
23+
ON
24+
templates.created_by = visible_users.id;
25+
26+
COMMENT ON VIEW template_with_users IS 'Joins in the username + avatar url of the created by user.';
27+
28+
COMMIT;

coderd/database/models.go

Lines changed: 3 additions & 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: 12 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries/templates.sql

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,8 @@ FROM build_times
174174
UPDATE
175175
templates
176176
SET
177-
require_active_version = $2
177+
require_active_version = $2,
178+
deprecated = $3
178179
WHERE
179180
id = $1
180181
;

coderd/templates.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -584,6 +584,11 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
584584
if req.AutostopRequirement.Weeks > schedule.MaxTemplateAutostopRequirementWeeks {
585585
validErrs = append(validErrs, codersdk.ValidationError{Field: "autostop_requirement.weeks", Detail: fmt.Sprintf("Must be less than %d.", schedule.MaxTemplateAutostopRequirementWeeks)})
586586
}
587+
// Defaults to the existing.
588+
deprecatedMessage := template.Deprecated
589+
if req.DeprecatedMessage != nil {
590+
deprecatedMessage = *req.DeprecatedMessage
591+
}
587592

588593
// The minimum valid value for a dormant TTL is 1 minute. This is
589594
// to ensure an uninformed user does not send an unintentionally
@@ -624,7 +629,8 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
624629
req.FailureTTLMillis == time.Duration(template.FailureTTL).Milliseconds() &&
625630
req.TimeTilDormantMillis == time.Duration(template.TimeTilDormant).Milliseconds() &&
626631
req.TimeTilDormantAutoDeleteMillis == time.Duration(template.TimeTilDormantAutoDelete).Milliseconds() &&
627-
req.RequireActiveVersion == template.RequireActiveVersion {
632+
req.RequireActiveVersion == template.RequireActiveVersion &&
633+
(deprecatedMessage == template.Deprecated) {
628634
return nil
629635
}
630636

@@ -648,9 +654,10 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
648654
return xerrors.Errorf("update template metadata: %w", err)
649655
}
650656

651-
if template.RequireActiveVersion != req.RequireActiveVersion {
657+
if template.RequireActiveVersion != req.RequireActiveVersion || deprecatedMessage != template.Deprecated {
652658
err = (*api.AccessControlStore.Load()).SetTemplateAccessControl(ctx, tx, template.ID, dbauthz.TemplateAccessControl{
653659
RequireActiveVersion: req.RequireActiveVersion,
660+
Deprecated: deprecatedMessage,
654661
})
655662
if err != nil {
656663
return xerrors.Errorf("set template access control: %w", err)
@@ -804,6 +811,7 @@ func (api *API) convertTemplates(templates []database.Template) []codersdk.Templ
804811
func (api *API) convertTemplate(
805812
template database.Template,
806813
) codersdk.Template {
814+
templateAccessControl := (*(api.Options.AccessControlStore.Load())).GetTemplateAccessControl(template)
807815
activeCount, _ := api.metricsCache.TemplateUniqueUsers(template.ID)
808816

809817
buildTimeStats := api.metricsCache.TemplateBuildTimeStats(template.ID)
@@ -843,6 +851,9 @@ func (api *API) convertTemplate(
843851
AutostartRequirement: codersdk.TemplateAutostartRequirement{
844852
DaysOfWeek: codersdk.BitmapToWeekdays(template.AutostartAllowedDays()),
845853
},
846-
RequireActiveVersion: template.RequireActiveVersion,
854+
// These values depend on entitlements and come from the templateAccessControl
855+
RequireActiveVersion: templateAccessControl.RequireActiveVersion,
856+
Deprecated: templateAccessControl.IsDeprecated(),
857+
DeprecatedMessage: templateAccessControl.Deprecated,
847858
}
848859
}

coderd/templates_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,29 @@ func TestPatchTemplateMeta(t *testing.T) {
516516
assert.Equal(t, database.AuditActionWrite, auditor.AuditLogs()[4].Action)
517517
})
518518

519+
t.Run("AGPL_Deprecated", func(t *testing.T) {
520+
t.Parallel()
521+
522+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: false})
523+
user := coderdtest.CreateFirstUser(t, client)
524+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
525+
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
526+
527+
req := codersdk.UpdateTemplateMeta{
528+
DeprecatedMessage: ptr.Ref("APGL cannot deprecate"),
529+
}
530+
531+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
532+
defer cancel()
533+
534+
updated, err := client.UpdateTemplateMeta(ctx, template.ID, req)
535+
require.NoError(t, err)
536+
assert.Greater(t, updated.UpdatedAt, template.UpdatedAt)
537+
// AGPL cannot deprecate, expect no change
538+
assert.False(t, updated.Deprecated)
539+
assert.Empty(t, updated.DeprecatedMessage)
540+
})
541+
519542
t.Run("NoDefaultTTL", func(t *testing.T) {
520543
t.Parallel()
521544

coderd/workspaces.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,17 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
394394
return
395395
}
396396

397+
templateAccessControl := (*(api.AccessControlStore.Load())).GetTemplateAccessControl(template)
398+
if templateAccessControl.IsDeprecated() {
399+
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
400+
Message: fmt.Sprintf("Template %q has been deprecated, and cannot be used to create a new workspace.", template.Name),
401+
// Pass the deprecated message to the user.
402+
Detail: templateAccessControl.Deprecated,
403+
Validations: nil,
404+
})
405+
return
406+
}
407+
397408
if organization.ID != template.OrganizationID {
398409
httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{
399410
Message: fmt.Sprintf("Template is not in organization %q.", organization.Name),

0 commit comments

Comments
 (0)