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:
Generated
+6
@@ -10057,6 +10057,12 @@ const docTemplate = `{
|
||||
"default_ttl_ms": {
|
||||
"type": "integer"
|
||||
},
|
||||
"deprecated": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"deprecation_message": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
Generated
+6
@@ -9085,6 +9085,12 @@
|
||||
"default_ttl_ms": {
|
||||
"type": "integer"
|
||||
},
|
||||
"deprecated": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"deprecation_message": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
;
|
||||
|
||||
+33
-3
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user