mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Generated
+5
-1
@@ -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;
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
;
|
||||
|
||||
Reference in New Issue
Block a user