From c2bc801f830b67fc96fb8fd2989c90394b50060b Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 15 May 2025 17:55:17 -0500 Subject: [PATCH] chore: add 'classic_parameter_flow' column setting to templates (#17828) We are forcing users to try the dynamic parameter experience first. Currently this setting only comes into effect if an experiment is enabled. --- coderd/apidoc/docs.go | 3 ++ coderd/apidoc/swagger.json | 3 ++ coderd/database/dbmem/dbmem.go | 1 + coderd/database/dump.sql | 6 ++- ...27_version_dynamic_parameter_flow.down.sql | 28 ++++++++++ ...0327_version_dynamic_parameter_flow.up.sql | 36 +++++++++++++ coderd/database/modelqueries.go | 1 + coderd/database/models.go | 3 ++ coderd/database/queries.sql.go | 19 ++++--- coderd/database/queries/templates.sql | 3 +- coderd/templates.go | 17 ++++-- coderd/templates_test.go | 35 +++++++++++++ codersdk/templates.go | 8 +++ docs/admin/security/audit-logs.md | 52 +++++++++---------- docs/reference/api/schemas.md | 4 +- docs/reference/api/templates.md | 20 ++++--- enterprise/audit/table.go | 1 + site/src/api/typesGenerated.ts | 2 + .../TemplateSettingsForm.tsx | 30 +++++++++++ .../TemplateSettingsPage.test.tsx | 1 + site/src/testHelpers/entities.ts | 1 + 21 files changed, 229 insertions(+), 45 deletions(-) create mode 100644 coderd/database/migrations/000327_version_dynamic_parameter_flow.down.sql create mode 100644 coderd/database/migrations/000327_version_dynamic_parameter_flow.up.sql diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 157cb30383..f744b98895 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -15573,6 +15573,9 @@ const docTemplate = `{ "updated_at": { "type": "string", "format": "date-time" + }, + "use_classic_parameter_flow": { + "type": "boolean" } } }, diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 692065af3c..1859a4f6f6 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -14167,6 +14167,9 @@ "updated_at": { "type": "string", "format": "date-time" + }, + "use_classic_parameter_flow": { + "type": "boolean" } } }, diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index ac9b601bee..fc5a10cafc 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -11084,6 +11084,7 @@ func (q *FakeQuerier) UpdateTemplateMetaByID(_ context.Context, arg database.Upd tpl.GroupACL = arg.GroupACL tpl.AllowUserCancelWorkspaceJobs = arg.AllowUserCancelWorkspaceJobs tpl.MaxPortSharingLevel = arg.MaxPortSharingLevel + tpl.UseClassicParameterFlow = arg.UseClassicParameterFlow q.templates[idx] = tpl return nil } diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 0b1356ede1..2f23b3ad4c 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -1560,7 +1560,8 @@ CREATE TABLE templates ( require_active_version boolean DEFAULT false NOT NULL, deprecated text DEFAULT ''::text NOT NULL, activity_bump bigint DEFAULT '3600000000000'::bigint NOT NULL, - max_port_sharing_level app_sharing_level DEFAULT 'owner'::app_sharing_level NOT NULL + max_port_sharing_level app_sharing_level DEFAULT 'owner'::app_sharing_level NOT NULL, + use_classic_parameter_flow boolean DEFAULT false NOT NULL ); COMMENT ON COLUMN templates.default_ttl IS 'The default duration for autostop for workspaces created from this template.'; @@ -1581,6 +1582,8 @@ COMMENT ON COLUMN templates.autostart_block_days_of_week IS 'A bitmap of days of 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.'; +COMMENT ON COLUMN templates.use_classic_parameter_flow IS 'Determines whether to default to the dynamic parameter creation flow for this template or continue using the legacy classic parameter creation flow.This is a template wide setting, the template admin can revert to the classic flow if there are any issues. An escape hatch is required, as workspace creation is a core workflow and cannot break. This column will be removed when the dynamic parameter creation flow is stable.'; + CREATE VIEW template_with_names AS SELECT templates.id, templates.created_at, @@ -1610,6 +1613,7 @@ CREATE VIEW template_with_names AS templates.deprecated, templates.activity_bump, templates.max_port_sharing_level, + templates.use_classic_parameter_flow, COALESCE(visible_users.avatar_url, ''::text) AS created_by_avatar_url, COALESCE(visible_users.username, ''::text) AS created_by_username, COALESCE(organizations.name, ''::text) AS organization_name, diff --git a/coderd/database/migrations/000327_version_dynamic_parameter_flow.down.sql b/coderd/database/migrations/000327_version_dynamic_parameter_flow.down.sql new file mode 100644 index 0000000000..6839abb73d --- /dev/null +++ b/coderd/database/migrations/000327_version_dynamic_parameter_flow.down.sql @@ -0,0 +1,28 @@ +DROP VIEW template_with_names; + +-- Drop the column +ALTER TABLE templates DROP COLUMN use_classic_parameter_flow; + + +CREATE VIEW + template_with_names +AS +SELECT + templates.*, + coalesce(visible_users.avatar_url, '') AS created_by_avatar_url, + coalesce(visible_users.username, '') AS created_by_username, + coalesce(organizations.name, '') AS organization_name, + coalesce(organizations.display_name, '') AS organization_display_name, + coalesce(organizations.icon, '') AS organization_icon +FROM + templates + LEFT JOIN + visible_users + ON + templates.created_by = visible_users.id + LEFT JOIN + organizations + ON templates.organization_id = organizations.id +; + +COMMENT ON VIEW template_with_names IS 'Joins in the display name information such as username, avatar, and organization name.'; diff --git a/coderd/database/migrations/000327_version_dynamic_parameter_flow.up.sql b/coderd/database/migrations/000327_version_dynamic_parameter_flow.up.sql new file mode 100644 index 0000000000..ba724b3fb8 --- /dev/null +++ b/coderd/database/migrations/000327_version_dynamic_parameter_flow.up.sql @@ -0,0 +1,36 @@ +-- Default to `false`. Users will have to manually opt back into the classic parameter flow. +-- We want the new experience to be tried first. +ALTER TABLE templates ADD COLUMN use_classic_parameter_flow BOOL NOT NULL DEFAULT false; + +COMMENT ON COLUMN templates.use_classic_parameter_flow IS + 'Determines whether to default to the dynamic parameter creation flow for this template ' + 'or continue using the legacy classic parameter creation flow.' + 'This is a template wide setting, the template admin can revert to the classic flow if there are any issues. ' + 'An escape hatch is required, as workspace creation is a core workflow and cannot break. ' + 'This column will be removed when the dynamic parameter creation flow is stable.'; + + +-- Update the template_with_names view by recreating it. +DROP VIEW template_with_names; +CREATE VIEW + template_with_names +AS +SELECT + templates.*, + coalesce(visible_users.avatar_url, '') AS created_by_avatar_url, + coalesce(visible_users.username, '') AS created_by_username, + coalesce(organizations.name, '') AS organization_name, + coalesce(organizations.display_name, '') AS organization_display_name, + coalesce(organizations.icon, '') AS organization_icon +FROM + templates + LEFT JOIN + visible_users + ON + templates.created_by = visible_users.id + LEFT JOIN + organizations + ON templates.organization_id = organizations.id +; + +COMMENT ON VIEW template_with_names IS 'Joins in the display name information such as username, avatar, and organization name.'; diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go index 1bf37ce0c0..4144c183de 100644 --- a/coderd/database/modelqueries.go +++ b/coderd/database/modelqueries.go @@ -117,6 +117,7 @@ func (q *sqlQuerier) GetAuthorizedTemplates(ctx context.Context, arg GetTemplate &i.Deprecated, &i.ActivityBump, &i.MaxPortSharingLevel, + &i.UseClassicParameterFlow, &i.CreatedByAvatarURL, &i.CreatedByUsername, &i.OrganizationName, diff --git a/coderd/database/models.go b/coderd/database/models.go index 3674af6ed6..ff49b8f471 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -3114,6 +3114,7 @@ type Template struct { Deprecated string `db:"deprecated" json:"deprecated"` ActivityBump int64 `db:"activity_bump" json:"activity_bump"` MaxPortSharingLevel AppSharingLevel `db:"max_port_sharing_level" json:"max_port_sharing_level"` + UseClassicParameterFlow bool `db:"use_classic_parameter_flow" json:"use_classic_parameter_flow"` CreatedByAvatarURL string `db:"created_by_avatar_url" json:"created_by_avatar_url"` CreatedByUsername string `db:"created_by_username" json:"created_by_username"` OrganizationName string `db:"organization_name" json:"organization_name"` @@ -3159,6 +3160,8 @@ type TemplateTable struct { Deprecated string `db:"deprecated" json:"deprecated"` ActivityBump int64 `db:"activity_bump" json:"activity_bump"` MaxPortSharingLevel AppSharingLevel `db:"max_port_sharing_level" json:"max_port_sharing_level"` + // Determines whether to default to the dynamic parameter creation flow for this template or continue using the legacy classic parameter creation flow.This is a template wide setting, the template admin can revert to the classic flow if there are any issues. An escape hatch is required, as workspace creation is a core workflow and cannot break. This column will be removed when the dynamic parameter creation flow is stable. + UseClassicParameterFlow bool `db:"use_classic_parameter_flow" json:"use_classic_parameter_flow"` } // Records aggregated usage statistics for templates/users. All usage is rounded up to the nearest minute. diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index a273b93732..ac08d72d0e 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -10427,7 +10427,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, 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, activity_bump, max_port_sharing_level, created_by_avatar_url, created_by_username, organization_name, organization_display_name, organization_icon + 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, 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, activity_bump, max_port_sharing_level, use_classic_parameter_flow, created_by_avatar_url, created_by_username, organization_name, organization_display_name, organization_icon FROM template_with_names WHERE @@ -10468,6 +10468,7 @@ func (q *sqlQuerier) GetTemplateByID(ctx context.Context, id uuid.UUID) (Templat &i.Deprecated, &i.ActivityBump, &i.MaxPortSharingLevel, + &i.UseClassicParameterFlow, &i.CreatedByAvatarURL, &i.CreatedByUsername, &i.OrganizationName, @@ -10479,7 +10480,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, 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, activity_bump, max_port_sharing_level, created_by_avatar_url, created_by_username, organization_name, organization_display_name, organization_icon + 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, 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, activity_bump, max_port_sharing_level, use_classic_parameter_flow, created_by_avatar_url, created_by_username, organization_name, organization_display_name, organization_icon FROM template_with_names AS templates WHERE @@ -10528,6 +10529,7 @@ func (q *sqlQuerier) GetTemplateByOrganizationAndName(ctx context.Context, arg G &i.Deprecated, &i.ActivityBump, &i.MaxPortSharingLevel, + &i.UseClassicParameterFlow, &i.CreatedByAvatarURL, &i.CreatedByUsername, &i.OrganizationName, @@ -10538,7 +10540,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, 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, activity_bump, max_port_sharing_level, created_by_avatar_url, created_by_username, organization_name, organization_display_name, organization_icon FROM template_with_names 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, 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, activity_bump, max_port_sharing_level, use_classic_parameter_flow, created_by_avatar_url, created_by_username, organization_name, organization_display_name, organization_icon FROM template_with_names AS templates ORDER BY (name, id) ASC ` @@ -10580,6 +10582,7 @@ func (q *sqlQuerier) GetTemplates(ctx context.Context) ([]Template, error) { &i.Deprecated, &i.ActivityBump, &i.MaxPortSharingLevel, + &i.UseClassicParameterFlow, &i.CreatedByAvatarURL, &i.CreatedByUsername, &i.OrganizationName, @@ -10601,7 +10604,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, 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, activity_bump, max_port_sharing_level, created_by_avatar_url, created_by_username, organization_name, organization_display_name, organization_icon + 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, 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, activity_bump, max_port_sharing_level, use_classic_parameter_flow, created_by_avatar_url, created_by_username, organization_name, organization_display_name, organization_icon FROM template_with_names AS templates WHERE @@ -10701,6 +10704,7 @@ func (q *sqlQuerier) GetTemplatesWithFilter(ctx context.Context, arg GetTemplate &i.Deprecated, &i.ActivityBump, &i.MaxPortSharingLevel, + &i.UseClassicParameterFlow, &i.CreatedByAvatarURL, &i.CreatedByUsername, &i.OrganizationName, @@ -10877,7 +10881,8 @@ SET display_name = $6, allow_user_cancel_workspace_jobs = $7, group_acl = $8, - max_port_sharing_level = $9 + max_port_sharing_level = $9, + use_classic_parameter_flow = $10 WHERE id = $1 ` @@ -10892,6 +10897,7 @@ type UpdateTemplateMetaByIDParams struct { AllowUserCancelWorkspaceJobs bool `db:"allow_user_cancel_workspace_jobs" json:"allow_user_cancel_workspace_jobs"` GroupACL TemplateACL `db:"group_acl" json:"group_acl"` MaxPortSharingLevel AppSharingLevel `db:"max_port_sharing_level" json:"max_port_sharing_level"` + UseClassicParameterFlow bool `db:"use_classic_parameter_flow" json:"use_classic_parameter_flow"` } func (q *sqlQuerier) UpdateTemplateMetaByID(ctx context.Context, arg UpdateTemplateMetaByIDParams) error { @@ -10905,6 +10911,7 @@ func (q *sqlQuerier) UpdateTemplateMetaByID(ctx context.Context, arg UpdateTempl arg.AllowUserCancelWorkspaceJobs, arg.GroupACL, arg.MaxPortSharingLevel, + arg.UseClassicParameterFlow, ) return err } @@ -18197,7 +18204,7 @@ LEFT JOIN LATERAL ( ) latest_build ON TRUE LEFT JOIN LATERAL ( 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, 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, activity_bump, max_port_sharing_level + 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, 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, activity_bump, max_port_sharing_level, use_classic_parameter_flow FROM templates WHERE diff --git a/coderd/database/queries/templates.sql b/coderd/database/queries/templates.sql index 84df9633a1..3a0d34885f 100644 --- a/coderd/database/queries/templates.sql +++ b/coderd/database/queries/templates.sql @@ -124,7 +124,8 @@ SET display_name = $6, allow_user_cancel_workspace_jobs = $7, group_acl = $8, - max_port_sharing_level = $9 + max_port_sharing_level = $9, + use_classic_parameter_flow = $10 WHERE id = $1 ; diff --git a/coderd/templates.go b/coderd/templates.go index 3b0393e84f..2a3e0326b1 100644 --- a/coderd/templates.go +++ b/coderd/templates.go @@ -728,6 +728,12 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) { return } + // Defaults to the existing. + classicTemplateFlow := template.UseClassicParameterFlow + if req.UseClassicParameterFlow != nil { + classicTemplateFlow = *req.UseClassicParameterFlow + } + var updated database.Template err = api.Database.InTx(func(tx database.Store) error { if req.Name == template.Name && @@ -747,6 +753,7 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) { req.TimeTilDormantAutoDeleteMillis == time.Duration(template.TimeTilDormantAutoDelete).Milliseconds() && req.RequireActiveVersion == template.RequireActiveVersion && (deprecationMessage == template.Deprecated) && + (classicTemplateFlow == template.UseClassicParameterFlow) && maxPortShareLevel == template.MaxPortSharingLevel { return nil } @@ -788,6 +795,7 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) { AllowUserCancelWorkspaceJobs: req.AllowUserCancelWorkspaceJobs, GroupACL: groupACL, MaxPortSharingLevel: maxPortShareLevel, + UseClassicParameterFlow: classicTemplateFlow, }) if err != nil { return xerrors.Errorf("update template metadata: %w", err) @@ -1066,10 +1074,11 @@ func (api *API) convertTemplate( DaysOfWeek: codersdk.BitmapToWeekdays(template.AutostartAllowedDays()), }, // These values depend on entitlements and come from the templateAccessControl - RequireActiveVersion: templateAccessControl.RequireActiveVersion, - Deprecated: templateAccessControl.IsDeprecated(), - DeprecationMessage: templateAccessControl.Deprecated, - MaxPortShareLevel: maxPortShareLevel, + RequireActiveVersion: templateAccessControl.RequireActiveVersion, + Deprecated: templateAccessControl.IsDeprecated(), + DeprecationMessage: templateAccessControl.Deprecated, + MaxPortShareLevel: maxPortShareLevel, + UseClassicParameterFlow: template.UseClassicParameterFlow, } } diff --git a/coderd/templates_test.go b/coderd/templates_test.go index 6f91139be4..f5fbe49741 100644 --- a/coderd/templates_test.go +++ b/coderd/templates_test.go @@ -1540,6 +1540,41 @@ func TestPatchTemplateMeta(t *testing.T) { require.False(t, template.Deprecated) }) }) + + t.Run("ClassicParameterFlow", func(t *testing.T) { + t.Parallel() + + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + require.False(t, template.UseClassicParameterFlow, "default is false") + + bTrue := true + bFalse := false + req := codersdk.UpdateTemplateMeta{ + UseClassicParameterFlow: &bTrue, + } + + ctx := testutil.Context(t, testutil.WaitLong) + + // set to true + updated, err := client.UpdateTemplateMeta(ctx, template.ID, req) + require.NoError(t, err) + assert.True(t, updated.UseClassicParameterFlow, "expected true") + + // noop + req.UseClassicParameterFlow = nil + updated, err = client.UpdateTemplateMeta(ctx, template.ID, req) + require.NoError(t, err) + assert.True(t, updated.UseClassicParameterFlow, "expected true") + + // back to false + req.UseClassicParameterFlow = &bFalse + updated, err = client.UpdateTemplateMeta(ctx, template.ID, req) + require.NoError(t, err) + assert.False(t, updated.UseClassicParameterFlow, "expected false") + }) } func TestDeleteTemplate(t *testing.T) { diff --git a/codersdk/templates.go b/codersdk/templates.go index 9e74887b53..c0ea8c4137 100644 --- a/codersdk/templates.go +++ b/codersdk/templates.go @@ -61,6 +61,8 @@ type Template struct { // template version. RequireActiveVersion bool `json:"require_active_version"` MaxPortShareLevel WorkspaceAgentPortShareLevel `json:"max_port_share_level"` + + UseClassicParameterFlow bool `json:"use_classic_parameter_flow"` } // WeekdaysToBitmap converts a list of weekdays to a bitmap in accordance with @@ -250,6 +252,12 @@ type UpdateTemplateMeta struct { // of the template. DisableEveryoneGroupAccess bool `json:"disable_everyone_group_access"` MaxPortShareLevel *WorkspaceAgentPortShareLevel `json:"max_port_share_level,omitempty"` + // UseClassicParameterFlow is a flag that switches the default behavior to use the classic + // parameter flow when creating a workspace. This only affects deployments with the experiment + // "dynamic-parameters" enabled. This setting will live for a period after the experiment is + // made the default. + // An "opt-out" is present in case the new feature breaks some existing templates. + UseClassicParameterFlow *bool `json:"use_classic_parameter_flow,omitempty"` } type TemplateExample struct { diff --git a/docs/admin/security/audit-logs.md b/docs/admin/security/audit-logs.md index 626f998f59..d0b2a46a9d 100644 --- a/docs/admin/security/audit-logs.md +++ b/docs/admin/security/audit-logs.md @@ -8,32 +8,32 @@ We track the following resources: -| Resource | | | -|----------------------------------------------------------|----------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| APIKey
login, logout, register, create, delete | |
FieldTracked
created_attrue
expires_attrue
hashed_secretfalse
idfalse
ip_addressfalse
last_usedtrue
lifetime_secondsfalse
login_typefalse
scopefalse
token_namefalse
updated_atfalse
user_idtrue
| -| AuditOAuthConvertState
| |
FieldTracked
created_attrue
expires_attrue
from_login_typetrue
to_login_typetrue
user_idtrue
| -| Group
create, write, delete | |
FieldTracked
avatar_urltrue
display_nametrue
idtrue
memberstrue
nametrue
organization_idfalse
quota_allowancetrue
sourcefalse
| -| AuditableOrganizationMember
| |
FieldTracked
created_attrue
organization_idfalse
rolestrue
updated_attrue
user_idtrue
usernametrue
| -| CustomRole
| |
FieldTracked
created_atfalse
display_nametrue
idfalse
nametrue
org_permissionstrue
organization_idfalse
site_permissionstrue
updated_atfalse
user_permissionstrue
| -| GitSSHKey
create | |
FieldTracked
created_atfalse
private_keytrue
public_keytrue
updated_atfalse
user_idtrue
| -| GroupSyncSettings
| |
FieldTracked
auto_create_missing_groupstrue
fieldtrue
legacy_group_name_mappingfalse
mappingtrue
regex_filtertrue
| -| HealthSettings
| |
FieldTracked
dismissed_healthcheckstrue
idfalse
| -| License
create, delete | |
FieldTracked
exptrue
idfalse
jwtfalse
uploaded_attrue
uuidtrue
| -| NotificationTemplate
| |
FieldTracked
actionstrue
body_templatetrue
enabled_by_defaulttrue
grouptrue
idfalse
kindtrue
methodtrue
nametrue
title_templatetrue
| -| NotificationsSettings
| |
FieldTracked
idfalse
notifier_pausedtrue
| -| OAuth2ProviderApp
| |
FieldTracked
callback_urltrue
created_atfalse
icontrue
idfalse
nametrue
updated_atfalse
| -| OAuth2ProviderAppSecret
| |
FieldTracked
app_idfalse
created_atfalse
display_secretfalse
hashed_secretfalse
idfalse
last_used_atfalse
secret_prefixfalse
| -| Organization
| |
FieldTracked
created_atfalse
deletedtrue
descriptiontrue
display_nametrue
icontrue
idfalse
is_defaulttrue
nametrue
updated_attrue
| -| OrganizationSyncSettings
| |
FieldTracked
assign_defaulttrue
fieldtrue
mappingtrue
| -| RoleSyncSettings
| |
FieldTracked
fieldtrue
mappingtrue
| -| Template
write, delete | |
FieldTracked
active_version_idtrue
activity_bumptrue
allow_user_autostarttrue
allow_user_autostoptrue
allow_user_cancel_workspace_jobstrue
autostart_block_days_of_weektrue
autostop_requirement_days_of_weektrue
autostop_requirement_weekstrue
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_usernamefalse
default_ttltrue
deletedfalse
deprecatedtrue
descriptiontrue
display_nametrue
failure_ttltrue
group_acltrue
icontrue
idtrue
max_port_sharing_leveltrue
nametrue
organization_display_namefalse
organization_iconfalse
organization_idfalse
organization_namefalse
provisionertrue
require_active_versiontrue
time_til_dormanttrue
time_til_dormant_autodeletetrue
updated_atfalse
user_acltrue
| -| TemplateVersion
create, write | |
FieldTracked
archivedtrue
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_usernamefalse
external_auth_providersfalse
idtrue
job_idfalse
messagefalse
nametrue
organization_idfalse
readmetrue
source_example_idfalse
template_idtrue
updated_atfalse
| -| User
create, write, delete | |
FieldTracked
avatar_urlfalse
created_atfalse
deletedtrue
emailtrue
github_com_user_idfalse
hashed_one_time_passcodefalse
hashed_passwordtrue
idtrue
is_systemtrue
last_seen_atfalse
login_typetrue
nametrue
one_time_passcode_expires_attrue
quiet_hours_scheduletrue
rbac_rolestrue
statustrue
updated_atfalse
usernametrue
| -| WorkspaceAgent
connect, disconnect | |
FieldTracked
api_key_scopefalse
api_versionfalse
architecturefalse
auth_instance_idfalse
auth_tokenfalse
connection_timeout_secondsfalse
created_atfalse
directoryfalse
disconnected_atfalse
display_appsfalse
display_orderfalse
environment_variablesfalse
expanded_directoryfalse
first_connected_atfalse
idfalse
instance_metadatafalse
last_connected_atfalse
last_connected_replica_idfalse
lifecycle_statefalse
logs_lengthfalse
logs_overflowedfalse
motd_filefalse
namefalse
operating_systemfalse
parent_idfalse
ready_atfalse
resource_idfalse
resource_metadatafalse
started_atfalse
subsystemsfalse
troubleshooting_urlfalse
updated_atfalse
versionfalse
| -| WorkspaceApp
open, close | |
FieldTracked
agent_idfalse
commandfalse
created_atfalse
display_namefalse
display_orderfalse
externalfalse
healthfalse
healthcheck_intervalfalse
healthcheck_thresholdfalse
healthcheck_urlfalse
hiddenfalse
iconfalse
idfalse
open_infalse
sharing_levelfalse
slugfalse
subdomainfalse
urlfalse
| -| WorkspaceBuild
start, stop | |
FieldTracked
build_numberfalse
created_atfalse
daily_costfalse
deadlinefalse
idfalse
initiator_by_avatar_urlfalse
initiator_by_usernamefalse
initiator_idfalse
job_idfalse
max_deadlinefalse
provisioner_statefalse
reasonfalse
template_version_idtrue
template_version_preset_idfalse
transitionfalse
updated_atfalse
workspace_idfalse
| -| WorkspaceProxy
| |
FieldTracked
created_attrue
deletedfalse
derp_enabledtrue
derp_onlytrue
display_nametrue
icontrue
idtrue
nametrue
region_idtrue
token_hashed_secrettrue
updated_atfalse
urltrue
versiontrue
wildcard_hostnametrue
| -| WorkspaceTable
| |
FieldTracked
automatic_updatestrue
autostart_scheduletrue
created_atfalse
deletedfalse
deleting_attrue
dormant_attrue
favoritetrue
idtrue
last_used_atfalse
nametrue
next_start_attrue
organization_idfalse
owner_idtrue
template_idtrue
ttltrue
updated_atfalse
| +| Resource | | | +|----------------------------------------------------------|----------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| APIKey
login, logout, register, create, delete | |
FieldTracked
created_attrue
expires_attrue
hashed_secretfalse
idfalse
ip_addressfalse
last_usedtrue
lifetime_secondsfalse
login_typefalse
scopefalse
token_namefalse
updated_atfalse
user_idtrue
| +| AuditOAuthConvertState
| |
FieldTracked
created_attrue
expires_attrue
from_login_typetrue
to_login_typetrue
user_idtrue
| +| Group
create, write, delete | |
FieldTracked
avatar_urltrue
display_nametrue
idtrue
memberstrue
nametrue
organization_idfalse
quota_allowancetrue
sourcefalse
| +| AuditableOrganizationMember
| |
FieldTracked
created_attrue
organization_idfalse
rolestrue
updated_attrue
user_idtrue
usernametrue
| +| CustomRole
| |
FieldTracked
created_atfalse
display_nametrue
idfalse
nametrue
org_permissionstrue
organization_idfalse
site_permissionstrue
updated_atfalse
user_permissionstrue
| +| GitSSHKey
create | |
FieldTracked
created_atfalse
private_keytrue
public_keytrue
updated_atfalse
user_idtrue
| +| GroupSyncSettings
| |
FieldTracked
auto_create_missing_groupstrue
fieldtrue
legacy_group_name_mappingfalse
mappingtrue
regex_filtertrue
| +| HealthSettings
| |
FieldTracked
dismissed_healthcheckstrue
idfalse
| +| License
create, delete | |
FieldTracked
exptrue
idfalse
jwtfalse
uploaded_attrue
uuidtrue
| +| NotificationTemplate
| |
FieldTracked
actionstrue
body_templatetrue
enabled_by_defaulttrue
grouptrue
idfalse
kindtrue
methodtrue
nametrue
title_templatetrue
| +| NotificationsSettings
| |
FieldTracked
idfalse
notifier_pausedtrue
| +| OAuth2ProviderApp
| |
FieldTracked
callback_urltrue
created_atfalse
icontrue
idfalse
nametrue
updated_atfalse
| +| OAuth2ProviderAppSecret
| |
FieldTracked
app_idfalse
created_atfalse
display_secretfalse
hashed_secretfalse
idfalse
last_used_atfalse
secret_prefixfalse
| +| Organization
| |
FieldTracked
created_atfalse
deletedtrue
descriptiontrue
display_nametrue
icontrue
idfalse
is_defaulttrue
nametrue
updated_attrue
| +| OrganizationSyncSettings
| |
FieldTracked
assign_defaulttrue
fieldtrue
mappingtrue
| +| RoleSyncSettings
| |
FieldTracked
fieldtrue
mappingtrue
| +| Template
write, delete | |
FieldTracked
active_version_idtrue
activity_bumptrue
allow_user_autostarttrue
allow_user_autostoptrue
allow_user_cancel_workspace_jobstrue
autostart_block_days_of_weektrue
autostop_requirement_days_of_weektrue
autostop_requirement_weekstrue
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_usernamefalse
default_ttltrue
deletedfalse
deprecatedtrue
descriptiontrue
display_nametrue
failure_ttltrue
group_acltrue
icontrue
idtrue
max_port_sharing_leveltrue
nametrue
organization_display_namefalse
organization_iconfalse
organization_idfalse
organization_namefalse
provisionertrue
require_active_versiontrue
time_til_dormanttrue
time_til_dormant_autodeletetrue
updated_atfalse
use_classic_parameter_flowtrue
user_acltrue
| +| TemplateVersion
create, write | |
FieldTracked
archivedtrue
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_usernamefalse
external_auth_providersfalse
idtrue
job_idfalse
messagefalse
nametrue
organization_idfalse
readmetrue
source_example_idfalse
template_idtrue
updated_atfalse
| +| User
create, write, delete | |
FieldTracked
avatar_urlfalse
created_atfalse
deletedtrue
emailtrue
github_com_user_idfalse
hashed_one_time_passcodefalse
hashed_passwordtrue
idtrue
is_systemtrue
last_seen_atfalse
login_typetrue
nametrue
one_time_passcode_expires_attrue
quiet_hours_scheduletrue
rbac_rolestrue
statustrue
updated_atfalse
usernametrue
| +| WorkspaceAgent
connect, disconnect | |
FieldTracked
api_key_scopefalse
api_versionfalse
architecturefalse
auth_instance_idfalse
auth_tokenfalse
connection_timeout_secondsfalse
created_atfalse
directoryfalse
disconnected_atfalse
display_appsfalse
display_orderfalse
environment_variablesfalse
expanded_directoryfalse
first_connected_atfalse
idfalse
instance_metadatafalse
last_connected_atfalse
last_connected_replica_idfalse
lifecycle_statefalse
logs_lengthfalse
logs_overflowedfalse
motd_filefalse
namefalse
operating_systemfalse
parent_idfalse
ready_atfalse
resource_idfalse
resource_metadatafalse
started_atfalse
subsystemsfalse
troubleshooting_urlfalse
updated_atfalse
versionfalse
| +| WorkspaceApp
open, close | |
FieldTracked
agent_idfalse
commandfalse
created_atfalse
display_namefalse
display_orderfalse
externalfalse
healthfalse
healthcheck_intervalfalse
healthcheck_thresholdfalse
healthcheck_urlfalse
hiddenfalse
iconfalse
idfalse
open_infalse
sharing_levelfalse
slugfalse
subdomainfalse
urlfalse
| +| WorkspaceBuild
start, stop | |
FieldTracked
build_numberfalse
created_atfalse
daily_costfalse
deadlinefalse
idfalse
initiator_by_avatar_urlfalse
initiator_by_usernamefalse
initiator_idfalse
job_idfalse
max_deadlinefalse
provisioner_statefalse
reasonfalse
template_version_idtrue
template_version_preset_idfalse
transitionfalse
updated_atfalse
workspace_idfalse
| +| WorkspaceProxy
| |
FieldTracked
created_attrue
deletedfalse
derp_enabledtrue
derp_onlytrue
display_nametrue
icontrue
idtrue
nametrue
region_idtrue
token_hashed_secrettrue
updated_atfalse
urltrue
versiontrue
wildcard_hostnametrue
| +| WorkspaceTable
| |
FieldTracked
automatic_updatestrue
autostart_scheduletrue
created_atfalse
deletedfalse
deleting_attrue
dormant_attrue
favoritetrue
idtrue
last_used_atfalse
nametrue
next_start_attrue
organization_idfalse
owner_idtrue
template_idtrue
ttltrue
updated_atfalse
| diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index eeb0014bd5..a001b72100 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -6593,7 +6593,8 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith "require_active_version": true, "time_til_dormant_autodelete_ms": 0, "time_til_dormant_ms": 0, - "updated_at": "2019-08-24T14:15:22Z" + "updated_at": "2019-08-24T14:15:22Z", + "use_classic_parameter_flow": true } ``` @@ -6632,6 +6633,7 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith | `time_til_dormant_autodelete_ms` | integer | false | | | | `time_til_dormant_ms` | integer | false | | | | `updated_at` | string | false | | | +| `use_classic_parameter_flow` | boolean | false | | | #### Enumerated Values diff --git a/docs/reference/api/templates.md b/docs/reference/api/templates.md index e3f4432649..c662118868 100644 --- a/docs/reference/api/templates.md +++ b/docs/reference/api/templates.md @@ -78,7 +78,8 @@ To include deprecated templates, specify `deprecated:true` in the search query. "require_active_version": true, "time_til_dormant_autodelete_ms": 0, "time_til_dormant_ms": 0, - "updated_at": "2019-08-24T14:15:22Z" + "updated_at": "2019-08-24T14:15:22Z", + "use_classic_parameter_flow": true } ] ``` @@ -134,6 +135,7 @@ Restarts will only happen on weekdays in this list on weeks which line up with W |`» time_til_dormant_autodelete_ms`|integer|false||| |`» time_til_dormant_ms`|integer|false||| |`» updated_at`|string(date-time)|false||| +|`» use_classic_parameter_flow`|boolean|false||| #### Enumerated Values @@ -255,7 +257,8 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/templa "require_active_version": true, "time_til_dormant_autodelete_ms": 0, "time_til_dormant_ms": 0, - "updated_at": "2019-08-24T14:15:22Z" + "updated_at": "2019-08-24T14:15:22Z", + "use_classic_parameter_flow": true } ``` @@ -403,7 +406,8 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templat "require_active_version": true, "time_til_dormant_autodelete_ms": 0, "time_til_dormant_ms": 0, - "updated_at": "2019-08-24T14:15:22Z" + "updated_at": "2019-08-24T14:15:22Z", + "use_classic_parameter_flow": true } ``` @@ -802,7 +806,8 @@ To include deprecated templates, specify `deprecated:true` in the search query. "require_active_version": true, "time_til_dormant_autodelete_ms": 0, "time_til_dormant_ms": 0, - "updated_at": "2019-08-24T14:15:22Z" + "updated_at": "2019-08-24T14:15:22Z", + "use_classic_parameter_flow": true } ] ``` @@ -858,6 +863,7 @@ Restarts will only happen on weekdays in this list on weeks which line up with W |`» time_til_dormant_autodelete_ms`|integer|false||| |`» time_til_dormant_ms`|integer|false||| |`» updated_at`|string(date-time)|false||| +|`» use_classic_parameter_flow`|boolean|false||| #### Enumerated Values @@ -999,7 +1005,8 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template} \ "require_active_version": true, "time_til_dormant_autodelete_ms": 0, "time_til_dormant_ms": 0, - "updated_at": "2019-08-24T14:15:22Z" + "updated_at": "2019-08-24T14:15:22Z", + "use_classic_parameter_flow": true } ``` @@ -1128,7 +1135,8 @@ curl -X PATCH http://coder-server:8080/api/v2/templates/{template} \ "require_active_version": true, "time_til_dormant_autodelete_ms": 0, "time_til_dormant_ms": 0, - "updated_at": "2019-08-24T14:15:22Z" + "updated_at": "2019-08-24T14:15:22Z", + "use_classic_parameter_flow": true } ``` diff --git a/enterprise/audit/table.go b/enterprise/audit/table.go index fa6e64cce5..3c836c9442 100644 --- a/enterprise/audit/table.go +++ b/enterprise/audit/table.go @@ -115,6 +115,7 @@ var auditableResourcesTypes = map[any]map[string]Action{ "deprecated": ActionTrack, "max_port_sharing_level": ActionTrack, "activity_bump": ActionTrack, + "use_classic_parameter_flow": ActionTrack, }, &database.TemplateVersion{}: { "id": ActionTrack, diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 63dabab5bd..6c09014c4e 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -2586,6 +2586,7 @@ export interface Template { readonly time_til_dormant_autodelete_ms: number; readonly require_active_version: boolean; readonly max_port_share_level: WorkspaceAgentPortShareLevel; + readonly use_classic_parameter_flow: boolean; } // From codersdk/templates.go @@ -2956,6 +2957,7 @@ export interface UpdateTemplateMeta { readonly deprecation_message?: string; readonly disable_everyone_group_access: boolean; readonly max_port_share_level?: WorkspaceAgentPortShareLevel; + readonly use_classic_parameter_flow?: boolean; } // From codersdk/users.go diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx index e346cc91e1..cd01421b64 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx @@ -47,6 +47,7 @@ export const validationSchema = Yup.object({ allow_user_cancel_workspace_jobs: Yup.boolean(), icon: iconValidator, require_active_version: Yup.boolean(), + use_classic_parameter_flow: Yup.boolean(), deprecation_message: Yup.string(), max_port_sharing_level: Yup.string().oneOf(WorkspaceAppSharingLevels), }); @@ -89,6 +90,7 @@ export const TemplateSettingsForm: FC = ({ deprecation_message: template.deprecation_message, disable_everyone_group_access: false, max_port_share_level: template.max_port_share_level, + use_classic_parameter_flow: template.use_classic_parameter_flow, }, validationSchema, onSubmit, @@ -222,6 +224,34 @@ export const TemplateSettingsForm: FC = ({ } /> + + } + label={ + + Use classic workspace creation form + + + Show the original workspace creation form without dynamic + parameters or live updates. Recommended if your provisioners + aren't updated or the new form causes issues. + + Users can always manually switch experiences in the + workspace creation form. + + + + + } + /> diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx index 35ca25a0f3..7811458969 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx @@ -54,6 +54,7 @@ const validFormValues: FormValues = { require_active_version: false, disable_everyone_group_access: false, max_port_share_level: "owner", + use_classic_parameter_flow: false, }; const renderTemplateSettingsPage = async () => { diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 5cc689f0bc..b05ec1d869 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -824,6 +824,7 @@ export const MockTemplate: TypesGen.Template = { deprecated: false, deprecation_message: "", max_port_share_level: "public", + use_classic_parameter_flow: false, }; const MockTemplateVersionFiles: TemplateVersionFiles = {