feat: implement deprecated flag for templates to prevent new workspaces (#10745)

* feat: implement deprecated flag for templates to prevent new workspaces
* Add deprecated filter to template fetching
* Add deprecated to template table
* Add deprecated notice to template page
* Add ui to deprecate a template
This commit is contained in:
Steven Masley
2023-11-20 19:16:18 +00:00
committed by GitHub
parent d8df87d5ae
commit 5229d7fd3a
41 changed files with 593 additions and 66 deletions
+6
View File
@@ -10057,6 +10057,12 @@ const docTemplate = `{
"default_ttl_ms": {
"type": "integer"
},
"deprecated": {
"type": "boolean"
},
"deprecation_message": {
"type": "string"
},
"description": {
"type": "string"
},
+6
View File
@@ -9085,6 +9085,12 @@
"default_ttl_ms": {
"type": "integer"
},
"deprecated": {
"type": "boolean"
},
"deprecation_message": {
"type": "string"
},
"description": {
"type": "string"
},
+16 -1
View File
@@ -614,6 +614,21 @@ func CreateAnotherUserMutators(t testing.TB, client *codersdk.Client, organizati
return createAnotherUserRetry(t, client, organizationID, 5, roles, mutators...)
}
// AuthzUserSubject does not include the user's groups.
func AuthzUserSubject(user codersdk.User) rbac.Subject {
roles := make(rbac.RoleNames, 0, len(user.Roles))
for _, r := range user.Roles {
roles = append(roles, r.Name)
}
return rbac.Subject{
ID: user.ID.String(),
Roles: roles,
Groups: []string{},
Scope: rbac.ScopeAll,
}
}
func createAnotherUserRetry(t testing.TB, client *codersdk.Client, organizationID uuid.UUID, retries int, roles []string, mutators ...func(r *codersdk.CreateUserRequest)) (*codersdk.Client, codersdk.User) {
req := codersdk.CreateUserRequest{
Email: namesgenerator.GetRandomName(10) + "@coder.com",
@@ -689,7 +704,7 @@ func createAnotherUserRetry(t testing.TB, client *codersdk.Client, organizationI
siteRoles = append(siteRoles, r.Name)
}
_, err := client.UpdateUserRoles(context.Background(), user.ID.String(), codersdk.UpdateRoles{Roles: siteRoles})
user, err = client.UpdateUserRoles(context.Background(), user.ID.String(), codersdk.UpdateRoles{Roles: siteRoles})
require.NoError(t, err, "update site roles")
// Update org roles
+34 -2
View File
@@ -4,6 +4,7 @@ import (
"context"
"github.com/google/uuid"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/coderd/database"
)
@@ -18,6 +19,11 @@ type AccessControlStore interface {
type TemplateAccessControl struct {
RequireActiveVersion bool
Deprecated string
}
func (t TemplateAccessControl) IsDeprecated() bool {
return t.Deprecated != ""
}
// AGPLTemplateAccessControlStore always returns the defaults for access control
@@ -26,12 +32,38 @@ type AGPLTemplateAccessControlStore struct{}
var _ AccessControlStore = AGPLTemplateAccessControlStore{}
func (AGPLTemplateAccessControlStore) GetTemplateAccessControl(database.Template) TemplateAccessControl {
func (AGPLTemplateAccessControlStore) GetTemplateAccessControl(t database.Template) TemplateAccessControl {
return TemplateAccessControl{
RequireActiveVersion: false,
// AGPL cannot set deprecated templates, but it should return
// existing deprecated templates. This is erroring on the safe side
// if a license expires, we should not allow deprecated templates
// to be used for new workspaces.
Deprecated: t.Deprecated,
}
}
func (AGPLTemplateAccessControlStore) SetTemplateAccessControl(context.Context, database.Store, uuid.UUID, TemplateAccessControl) error {
func (AGPLTemplateAccessControlStore) SetTemplateAccessControl(ctx context.Context, store database.Store, id uuid.UUID, opts TemplateAccessControl) error {
// AGPL is allowed to unset deprecated templates.
if opts.Deprecated == "" {
// This does require fetching again to ensure other fields are not
// changed.
tpl, err := store.GetTemplateByID(ctx, id)
if err != nil {
return xerrors.Errorf("get template: %w", err)
}
if tpl.Deprecated != "" {
err := store.UpdateTemplateAccessControlByID(ctx, database.UpdateTemplateAccessControlByIDParams{
ID: id,
RequireActiveVersion: tpl.RequireActiveVersion,
Deprecated: opts.Deprecated,
})
if err != nil {
return xerrors.Errorf("update template access control: %w", err)
}
}
}
return nil
}
+4
View File
@@ -5905,6 +5905,7 @@ func (q *FakeQuerier) UpdateTemplateAccessControlByID(_ context.Context, arg dat
continue
}
q.templates[idx].RequireActiveVersion = arg.RequireActiveVersion
q.templates[idx].Deprecated = arg.Deprecated
return nil
}
@@ -6887,6 +6888,9 @@ func (q *FakeQuerier) GetAuthorizedTemplates(ctx context.Context, arg database.G
if arg.ExactName != "" && !strings.EqualFold(template.Name, arg.ExactName) {
continue
}
if arg.Deprecated.Valid && arg.Deprecated.Bool == (template.Deprecated != "") {
continue
}
if len(arg.IDs) > 0 {
match := false
+5 -1
View File
@@ -805,7 +805,8 @@ CREATE TABLE templates (
autostop_requirement_days_of_week smallint DEFAULT 0 NOT NULL,
autostop_requirement_weeks bigint DEFAULT 0 NOT NULL,
autostart_block_days_of_week smallint DEFAULT 0 NOT NULL,
require_active_version boolean DEFAULT false NOT NULL
require_active_version boolean DEFAULT false NOT NULL,
deprecated text DEFAULT ''::text NOT NULL
);
COMMENT ON COLUMN templates.default_ttl IS 'The default duration for autostop for workspaces created from this template.';
@@ -824,6 +825,8 @@ COMMENT ON COLUMN templates.autostop_requirement_weeks IS 'The number of weeks b
COMMENT ON COLUMN templates.autostart_block_days_of_week IS 'A bitmap of days of week that autostart of a workspace is not allowed. Default allows all days. This is intended as a cost savings measure to prevent auto start on weekends (for example).';
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.';
CREATE VIEW template_with_users AS
SELECT templates.id,
templates.created_at,
@@ -851,6 +854,7 @@ CREATE VIEW template_with_users AS
templates.autostop_requirement_weeks,
templates.autostart_block_days_of_week,
templates.require_active_version,
templates.deprecated,
COALESCE(visible_users.avatar_url, ''::text) AS created_by_avatar_url,
COALESCE(visible_users.username, ''::text) AS created_by_username
FROM (public.templates
@@ -0,0 +1,24 @@
BEGIN;
DROP VIEW template_with_users;
ALTER TABLE templates
DROP COLUMN deprecated;
CREATE VIEW
template_with_users
AS
SELECT
templates.*,
coalesce(visible_users.avatar_url, '') AS created_by_avatar_url,
coalesce(visible_users.username, '') AS created_by_username
FROM
templates
LEFT JOIN
visible_users
ON
templates.created_by = visible_users.id;
COMMENT ON VIEW template_with_users IS 'Joins in the username + avatar url of the created by user.';
COMMIT;
@@ -0,0 +1,28 @@
BEGIN;
-- The view will be rebuilt with the new column
DROP VIEW template_with_users;
ALTER TABLE templates
ADD COLUMN deprecated TEXT NOT NULL DEFAULT '';
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.';
-- Restore the old version of the template_with_users view.
CREATE VIEW
template_with_users
AS
SELECT
templates.*,
coalesce(visible_users.avatar_url, '') AS created_by_avatar_url,
coalesce(visible_users.username, '') AS created_by_username
FROM
templates
LEFT JOIN
visible_users
ON
templates.created_by = visible_users.id;
COMMENT ON VIEW template_with_users IS 'Joins in the username + avatar url of the created by user.';
COMMIT;
+2
View File
@@ -52,6 +52,7 @@ func (q *sqlQuerier) GetAuthorizedTemplates(ctx context.Context, arg GetTemplate
arg.OrganizationID,
arg.ExactName,
pq.Array(arg.IDs),
arg.Deprecated,
)
if err != nil {
return nil, err
@@ -87,6 +88,7 @@ func (q *sqlQuerier) GetAuthorizedTemplates(ctx context.Context, arg GetTemplate
&i.AutostopRequirementWeeks,
&i.AutostartBlockDaysOfWeek,
&i.RequireActiveVersion,
&i.Deprecated,
&i.CreatedByAvatarURL,
&i.CreatedByUsername,
); err != nil {
+3
View File
@@ -1966,6 +1966,7 @@ type Template struct {
AutostopRequirementWeeks int64 `db:"autostop_requirement_weeks" json:"autostop_requirement_weeks"`
AutostartBlockDaysOfWeek int16 `db:"autostart_block_days_of_week" json:"autostart_block_days_of_week"`
RequireActiveVersion bool `db:"require_active_version" json:"require_active_version"`
Deprecated string `db:"deprecated" json:"deprecated"`
CreatedByAvatarURL sql.NullString `db:"created_by_avatar_url" json:"created_by_avatar_url"`
CreatedByUsername string `db:"created_by_username" json:"created_by_username"`
}
@@ -2005,6 +2006,8 @@ type TemplateTable struct {
// A bitmap of days of week that autostart of a workspace is not allowed. Default allows all days. This is intended as a cost savings measure to prevent auto start on weekends (for example).
AutostartBlockDaysOfWeek int16 `db:"autostart_block_days_of_week" json:"autostart_block_days_of_week"`
RequireActiveVersion bool `db:"require_active_version" json:"require_active_version"`
// 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.
Deprecated string `db:"deprecated" json:"deprecated"`
}
// Joins in the username + avatar url of the created by user.
+29 -10
View File
@@ -5183,7 +5183,7 @@ func (q *sqlQuerier) GetTemplateAverageBuildTime(ctx context.Context, arg GetTem
const getTemplateByID = `-- name: GetTemplateByID :one
SELECT
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, created_by_avatar_url, created_by_username
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, created_by_avatar_url, created_by_username
FROM
template_with_users
WHERE
@@ -5222,6 +5222,7 @@ func (q *sqlQuerier) GetTemplateByID(ctx context.Context, id uuid.UUID) (Templat
&i.AutostopRequirementWeeks,
&i.AutostartBlockDaysOfWeek,
&i.RequireActiveVersion,
&i.Deprecated,
&i.CreatedByAvatarURL,
&i.CreatedByUsername,
)
@@ -5230,7 +5231,7 @@ func (q *sqlQuerier) GetTemplateByID(ctx context.Context, id uuid.UUID) (Templat
const getTemplateByOrganizationAndName = `-- name: GetTemplateByOrganizationAndName :one
SELECT
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, created_by_avatar_url, created_by_username
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, created_by_avatar_url, created_by_username
FROM
template_with_users AS templates
WHERE
@@ -5277,6 +5278,7 @@ func (q *sqlQuerier) GetTemplateByOrganizationAndName(ctx context.Context, arg G
&i.AutostopRequirementWeeks,
&i.AutostartBlockDaysOfWeek,
&i.RequireActiveVersion,
&i.Deprecated,
&i.CreatedByAvatarURL,
&i.CreatedByUsername,
)
@@ -5284,7 +5286,7 @@ func (q *sqlQuerier) GetTemplateByOrganizationAndName(ctx context.Context, arg G
}
const getTemplates = `-- name: GetTemplates :many
SELECT id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, created_by_avatar_url, created_by_username FROM template_with_users AS templates
SELECT id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, created_by_avatar_url, created_by_username FROM template_with_users AS templates
ORDER BY (name, id) ASC
`
@@ -5324,6 +5326,7 @@ func (q *sqlQuerier) GetTemplates(ctx context.Context) ([]Template, error) {
&i.AutostopRequirementWeeks,
&i.AutostartBlockDaysOfWeek,
&i.RequireActiveVersion,
&i.Deprecated,
&i.CreatedByAvatarURL,
&i.CreatedByUsername,
); err != nil {
@@ -5342,7 +5345,7 @@ func (q *sqlQuerier) GetTemplates(ctx context.Context) ([]Template, error) {
const getTemplatesWithFilter = `-- name: GetTemplatesWithFilter :many
SELECT
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, created_by_avatar_url, created_by_username
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, created_by_avatar_url, created_by_username
FROM
template_with_users AS templates
WHERE
@@ -5366,16 +5369,28 @@ WHERE
id = ANY($4)
ELSE true
END
-- Filter by deprecated
AND CASE
WHEN $5 :: boolean IS NOT NULL THEN
CASE
WHEN $5 :: boolean THEN
deprecated != ''
ELSE
deprecated = ''
END
ELSE true
END
-- Authorize Filter clause will be injected below in GetAuthorizedTemplates
-- @authorize_filter
ORDER BY (name, id) ASC
`
type GetTemplatesWithFilterParams struct {
Deleted bool `db:"deleted" json:"deleted"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
ExactName string `db:"exact_name" json:"exact_name"`
IDs []uuid.UUID `db:"ids" json:"ids"`
Deleted bool `db:"deleted" json:"deleted"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
ExactName string `db:"exact_name" json:"exact_name"`
IDs []uuid.UUID `db:"ids" json:"ids"`
Deprecated sql.NullBool `db:"deprecated" json:"deprecated"`
}
func (q *sqlQuerier) GetTemplatesWithFilter(ctx context.Context, arg GetTemplatesWithFilterParams) ([]Template, error) {
@@ -5384,6 +5399,7 @@ func (q *sqlQuerier) GetTemplatesWithFilter(ctx context.Context, arg GetTemplate
arg.OrganizationID,
arg.ExactName,
pq.Array(arg.IDs),
arg.Deprecated,
)
if err != nil {
return nil, err
@@ -5419,6 +5435,7 @@ func (q *sqlQuerier) GetTemplatesWithFilter(ctx context.Context, arg GetTemplate
&i.AutostopRequirementWeeks,
&i.AutostartBlockDaysOfWeek,
&i.RequireActiveVersion,
&i.Deprecated,
&i.CreatedByAvatarURL,
&i.CreatedByUsername,
); err != nil {
@@ -5519,7 +5536,8 @@ const updateTemplateAccessControlByID = `-- name: UpdateTemplateAccessControlByI
UPDATE
templates
SET
require_active_version = $2
require_active_version = $2,
deprecated = $3
WHERE
id = $1
`
@@ -5527,10 +5545,11 @@ WHERE
type UpdateTemplateAccessControlByIDParams struct {
ID uuid.UUID `db:"id" json:"id"`
RequireActiveVersion bool `db:"require_active_version" json:"require_active_version"`
Deprecated string `db:"deprecated" json:"deprecated"`
}
func (q *sqlQuerier) UpdateTemplateAccessControlByID(ctx context.Context, arg UpdateTemplateAccessControlByIDParams) error {
_, err := q.db.ExecContext(ctx, updateTemplateAccessControlByID, arg.ID, arg.RequireActiveVersion)
_, err := q.db.ExecContext(ctx, updateTemplateAccessControlByID, arg.ID, arg.RequireActiveVersion, arg.Deprecated)
return err
}
+13 -1
View File
@@ -34,6 +34,17 @@ WHERE
id = ANY(@ids)
ELSE true
END
-- Filter by deprecated
AND CASE
WHEN sqlc.narg('deprecated') :: boolean IS NOT NULL THEN
CASE
WHEN sqlc.narg('deprecated') :: boolean THEN
deprecated != ''
ELSE
deprecated = ''
END
ELSE true
END
-- Authorize Filter clause will be injected below in GetAuthorizedTemplates
-- @authorize_filter
ORDER BY (name, id) ASC
@@ -174,7 +185,8 @@ FROM build_times
UPDATE
templates
SET
require_active_version = $2
require_active_version = $2,
deprecated = $3
WHERE
id = $1
;
+33 -3
View File
@@ -437,6 +437,24 @@ func (api *API) templatesByOrganization(rw http.ResponseWriter, r *http.Request)
ctx := r.Context()
organization := httpmw.OrganizationParam(r)
p := httpapi.NewQueryParamParser()
values := r.URL.Query()
deprecated := sql.NullBool{}
if values.Has("deprecated") {
deprecated = sql.NullBool{
Bool: p.Boolean(values, false, "deprecated"),
Valid: true,
}
}
if len(p.Errors) > 0 {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Invalid query params.",
Validations: p.Errors,
})
return
}
prepared, err := api.HTTPAuth.AuthorizeSQLFilter(r, rbac.ActionRead, rbac.ResourceTemplate.Type)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
@@ -449,6 +467,7 @@ func (api *API) templatesByOrganization(rw http.ResponseWriter, r *http.Request)
// Filter templates based on rbac permissions
templates, err := api.Database.GetAuthorizedTemplates(ctx, database.GetTemplatesWithFilterParams{
OrganizationID: organization.ID,
Deprecated: deprecated,
}, prepared)
if errors.Is(err, sql.ErrNoRows) {
err = nil
@@ -584,6 +603,11 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
if req.AutostopRequirement.Weeks > schedule.MaxTemplateAutostopRequirementWeeks {
validErrs = append(validErrs, codersdk.ValidationError{Field: "autostop_requirement.weeks", Detail: fmt.Sprintf("Must be less than %d.", schedule.MaxTemplateAutostopRequirementWeeks)})
}
// Defaults to the existing.
deprecationMessage := template.Deprecated
if req.DeprecationMessage != nil {
deprecationMessage = *req.DeprecationMessage
}
// The minimum valid value for a dormant TTL is 1 minute. This is
// to ensure an uninformed user does not send an unintentionally
@@ -624,7 +648,8 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
req.FailureTTLMillis == time.Duration(template.FailureTTL).Milliseconds() &&
req.TimeTilDormantMillis == time.Duration(template.TimeTilDormant).Milliseconds() &&
req.TimeTilDormantAutoDeleteMillis == time.Duration(template.TimeTilDormantAutoDelete).Milliseconds() &&
req.RequireActiveVersion == template.RequireActiveVersion {
req.RequireActiveVersion == template.RequireActiveVersion &&
(deprecationMessage == template.Deprecated) {
return nil
}
@@ -648,9 +673,10 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
return xerrors.Errorf("update template metadata: %w", err)
}
if template.RequireActiveVersion != req.RequireActiveVersion {
if template.RequireActiveVersion != req.RequireActiveVersion || deprecationMessage != template.Deprecated {
err = (*api.AccessControlStore.Load()).SetTemplateAccessControl(ctx, tx, template.ID, dbauthz.TemplateAccessControl{
RequireActiveVersion: req.RequireActiveVersion,
Deprecated: deprecationMessage,
})
if err != nil {
return xerrors.Errorf("set template access control: %w", err)
@@ -804,6 +830,7 @@ func (api *API) convertTemplates(templates []database.Template) []codersdk.Templ
func (api *API) convertTemplate(
template database.Template,
) codersdk.Template {
templateAccessControl := (*(api.Options.AccessControlStore.Load())).GetTemplateAccessControl(template)
activeCount, _ := api.metricsCache.TemplateUniqueUsers(template.ID)
buildTimeStats := api.metricsCache.TemplateBuildTimeStats(template.ID)
@@ -843,6 +870,9 @@ func (api *API) convertTemplate(
AutostartRequirement: codersdk.TemplateAutostartRequirement{
DaysOfWeek: codersdk.BitmapToWeekdays(template.AutostartAllowedDays()),
},
RequireActiveVersion: template.RequireActiveVersion,
// These values depend on entitlements and come from the templateAccessControl
RequireActiveVersion: templateAccessControl.RequireActiveVersion,
Deprecated: templateAccessControl.IsDeprecated(),
DeprecationMessage: templateAccessControl.Deprecated,
}
}
+76
View File
@@ -16,7 +16,9 @@ import (
"github.com/coder/coder/v2/coderd/audit"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/schedule"
"github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/codersdk"
@@ -516,6 +518,66 @@ func TestPatchTemplateMeta(t *testing.T) {
assert.Equal(t, database.AuditActionWrite, auditor.AuditLogs()[4].Action)
})
t.Run("AGPL_Deprecated", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: false})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
req := codersdk.UpdateTemplateMeta{
DeprecationMessage: ptr.Ref("APGL cannot deprecate"),
}
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
updated, err := client.UpdateTemplateMeta(ctx, template.ID, req)
require.NoError(t, err)
assert.Greater(t, updated.UpdatedAt, template.UpdatedAt)
// AGPL cannot deprecate, expect no change
assert.False(t, updated.Deprecated)
assert.Empty(t, updated.DeprecationMessage)
})
// AGPL cannot deprecate, but it can be unset
t.Run("AGPL_Unset_Deprecated", func(t *testing.T) {
t.Parallel()
owner, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{IncludeProvisionerDaemon: false})
user := coderdtest.CreateFirstUser(t, owner)
client, tplAdmin := coderdtest.CreateAnotherUser(t, owner, user.OrganizationID, rbac.RoleTemplateAdmin())
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
// nolint:gocritic // Setting up unit test data
err := db.UpdateTemplateAccessControlByID(dbauthz.As(ctx, coderdtest.AuthzUserSubject(tplAdmin)), database.UpdateTemplateAccessControlByIDParams{
ID: template.ID,
RequireActiveVersion: false,
Deprecated: "Some deprecated message",
})
require.NoError(t, err)
// Check that it is deprecated
got, err := client.Template(ctx, template.ID)
require.NoError(t, err)
require.NotEmpty(t, got.DeprecationMessage, "template is deprecated to start")
require.True(t, got.Deprecated, "template is deprecated to start")
req := codersdk.UpdateTemplateMeta{
DeprecationMessage: ptr.Ref(""),
}
updated, err := client.UpdateTemplateMeta(ctx, template.ID, req)
require.NoError(t, err)
assert.Greater(t, updated.UpdatedAt, template.UpdatedAt)
assert.False(t, updated.Deprecated)
assert.Empty(t, updated.DeprecationMessage)
})
t.Run("NoDefaultTTL", func(t *testing.T) {
t.Parallel()
@@ -543,6 +605,8 @@ func TestPatchTemplateMeta(t *testing.T) {
require.NoError(t, err)
assert.Greater(t, updated.UpdatedAt, template.UpdatedAt)
assert.Equal(t, req.DefaultTTLMillis, updated.DefaultTTLMillis)
assert.Empty(t, updated.DeprecationMessage)
assert.False(t, updated.Deprecated)
})
t.Run("DefaultTTLTooLow", func(t *testing.T) {
@@ -569,6 +633,8 @@ func TestPatchTemplateMeta(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, updated.UpdatedAt, template.UpdatedAt)
assert.Equal(t, updated.DefaultTTLMillis, template.DefaultTTLMillis)
assert.Empty(t, updated.DeprecationMessage)
assert.False(t, updated.Deprecated)
})
t.Run("MaxTTL", func(t *testing.T) {
@@ -634,6 +700,8 @@ func TestPatchTemplateMeta(t *testing.T) {
require.EqualValues(t, 2, atomic.LoadInt64(&setCalled))
require.EqualValues(t, 0, got.DefaultTTLMillis)
require.Equal(t, maxTTL.Milliseconds(), got.MaxTTLMillis)
require.Empty(t, got.DeprecationMessage)
require.False(t, got.Deprecated)
})
t.Run("DefaultTTLBigger", func(t *testing.T) {
@@ -692,6 +760,8 @@ func TestPatchTemplateMeta(t *testing.T) {
require.NoError(t, err)
require.Equal(t, defaultTTL.Milliseconds(), got.DefaultTTLMillis)
require.Zero(t, got.MaxTTLMillis)
require.Empty(t, got.DeprecationMessage)
require.False(t, got.Deprecated)
})
})
@@ -785,6 +855,8 @@ func TestPatchTemplateMeta(t *testing.T) {
require.Zero(t, got.FailureTTLMillis)
require.Zero(t, got.TimeTilDormantMillis)
require.Zero(t, got.TimeTilDormantAutoDeleteMillis)
require.Empty(t, got.DeprecationMessage)
require.False(t, got.Deprecated)
})
})
@@ -1036,6 +1108,8 @@ func TestPatchTemplateMeta(t *testing.T) {
require.NoError(t, err)
require.Equal(t, []string{"friday", "saturday"}, template.AutostopRequirement.DaysOfWeek)
require.EqualValues(t, 2, template.AutostopRequirement.Weeks)
require.Empty(t, template.DeprecationMessage)
require.False(t, template.Deprecated)
})
t.Run("Unset", func(t *testing.T) {
@@ -1146,6 +1220,8 @@ func TestPatchTemplateMeta(t *testing.T) {
require.NoError(t, err)
require.Empty(t, template.AutostopRequirement.DaysOfWeek)
require.EqualValues(t, 1, template.AutostopRequirement.Weeks)
require.Empty(t, template.DeprecationMessage)
require.False(t, template.Deprecated)
})
})
}
+11
View File
@@ -394,6 +394,17 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
return
}
templateAccessControl := (*(api.AccessControlStore.Load())).GetTemplateAccessControl(template)
if templateAccessControl.IsDeprecated() {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: fmt.Sprintf("Template %q has been deprecated, and cannot be used to create a new workspace.", template.Name),
// Pass the deprecated message to the user.
Detail: templateAccessControl.Deprecated,
Validations: nil,
})
return
}
if organization.ID != template.OrganizationID {
httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{
Message: fmt.Sprintf("Template is not in organization %q.", organization.Name),