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
+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
;