From 03d8456f0e17731fc7e758920f1a6b1df3441726 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 8 Nov 2022 19:23:13 +0100 Subject: [PATCH 01/25] Rename to nameValidator --- coderd/httpapi/{username.go => name.go} | 28 +++++++++++-------- .../{username_test.go => name_test.go} | 0 2 files changed, 17 insertions(+), 11 deletions(-) rename coderd/httpapi/{username.go => name.go} (87%) rename coderd/httpapi/{username_test.go => name_test.go} (100%) diff --git a/coderd/httpapi/username.go b/coderd/httpapi/name.go similarity index 87% rename from coderd/httpapi/username.go rename to coderd/httpapi/name.go index 89a9cce92016a..de051726bdb7f 100644 --- a/coderd/httpapi/username.go +++ b/coderd/httpapi/name.go @@ -15,17 +15,7 @@ var ( // UsernameValid returns whether the input string is a valid username. func UsernameValid(str string) error { - if len(str) > 32 { - return xerrors.New("must be <= 32 characters") - } - if len(str) < 1 { - return xerrors.New("must be >= 1 character") - } - matched := UsernameValidRegex.MatchString(str) - if !matched { - return xerrors.New("must be alphanumeric with hyphens") - } - return nil + return nameValid(str) } // UsernameFrom returns a best-effort username from the provided string. @@ -48,3 +38,19 @@ func UsernameFrom(str string) string { } return strings.ReplaceAll(namesgenerator.GetRandomName(1), "_", "-") } + +// nameValid returns whether the input string is a valid name. +// It is a generic validator for any name (user, workspace, etc.). +func nameValid(str string) error { + if len(str) > 32 { + return xerrors.New("must be <= 32 characters") + } + if len(str) < 1 { + return xerrors.New("must be >= 1 character") + } + matched := UsernameValidRegex.MatchString(str) + if !matched { + return xerrors.New("must be alphanumeric with hyphens") + } + return nil +} diff --git a/coderd/httpapi/username_test.go b/coderd/httpapi/name_test.go similarity index 100% rename from coderd/httpapi/username_test.go rename to coderd/httpapi/name_test.go From 9c7cae6578259b1d3a68e1afe5dfec66e9ec30c4 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 9 Nov 2022 10:02:34 +0100 Subject: [PATCH 02/25] Refactor: NameValid --- coderd/gitauth/config.go | 2 +- coderd/httpapi/httpapi.go | 2 +- coderd/httpapi/name.go | 13 ++++--------- coderd/httpapi/name_test.go | 4 ++-- coderd/userauth.go | 2 +- 5 files changed, 9 insertions(+), 14 deletions(-) diff --git a/coderd/gitauth/config.go b/coderd/gitauth/config.go index e4c6fdd97eba7..43d6263a5e447 100644 --- a/coderd/gitauth/config.go +++ b/coderd/gitauth/config.go @@ -47,7 +47,7 @@ func ConvertConfig(entries []codersdk.GitAuthConfig, accessURL *url.URL) ([]*Con // Default to the type. entry.ID = string(typ) } - if valid := httpapi.UsernameValid(entry.ID); valid != nil { + if valid := httpapi.NameValid(entry.ID); valid != nil { return nil, xerrors.Errorf("git auth provider %q doesn't have a valid id: %w", entry.ID, valid) } diff --git a/coderd/httpapi/httpapi.go b/coderd/httpapi/httpapi.go index d2f1c07de0277..091eae0f883f9 100644 --- a/coderd/httpapi/httpapi.go +++ b/coderd/httpapi/httpapi.go @@ -39,7 +39,7 @@ func init() { if !ok { return false } - valid := UsernameValid(str) + valid := NameValid(str) return valid == nil } for _, tag := range []string{"username", "template_name", "workspace_name"} { diff --git a/coderd/httpapi/name.go b/coderd/httpapi/name.go index de051726bdb7f..bb1786930a8f6 100644 --- a/coderd/httpapi/name.go +++ b/coderd/httpapi/name.go @@ -13,11 +13,6 @@ var ( usernameReplace = regexp.MustCompile("[^a-zA-Z0-9-]*") ) -// UsernameValid returns whether the input string is a valid username. -func UsernameValid(str string) error { - return nameValid(str) -} - // UsernameFrom returns a best-effort username from the provided string. // // It first attempts to validate the incoming string, which will @@ -25,7 +20,7 @@ func UsernameValid(str string) error { // the username from an email address. If no success happens during // these steps, a random username will be returned. func UsernameFrom(str string) string { - if valid := UsernameValid(str); valid == nil { + if valid := NameValid(str); valid == nil { return str } emailAt := strings.LastIndex(str, "@") @@ -33,15 +28,15 @@ func UsernameFrom(str string) string { str = str[:emailAt] } str = usernameReplace.ReplaceAllString(str, "") - if valid := UsernameValid(str); valid == nil { + if valid := NameValid(str); valid == nil { return str } return strings.ReplaceAll(namesgenerator.GetRandomName(1), "_", "-") } -// nameValid returns whether the input string is a valid name. +// NameValid returns whether the input string is a valid name. // It is a generic validator for any name (user, workspace, etc.). -func nameValid(str string) error { +func NameValid(str string) error { if len(str) > 32 { return xerrors.New("must be <= 32 characters") } diff --git a/coderd/httpapi/name_test.go b/coderd/httpapi/name_test.go index 547d5177c4e56..de79261457a9e 100644 --- a/coderd/httpapi/name_test.go +++ b/coderd/httpapi/name_test.go @@ -59,7 +59,7 @@ func TestValid(t *testing.T) { testCase := testCase t.Run(testCase.Username, func(t *testing.T) { t.Parallel() - valid := httpapi.UsernameValid(testCase.Username) + valid := httpapi.NameValid(testCase.Username) require.Equal(t, testCase.Valid, valid == nil) }) } @@ -92,7 +92,7 @@ func TestFrom(t *testing.T) { t.Parallel() converted := httpapi.UsernameFrom(testCase.From) t.Log(converted) - valid := httpapi.UsernameValid(converted) + valid := httpapi.NameValid(converted) require.True(t, valid == nil) if testCase.Match == "" { require.NotEqual(t, testCase.From, converted) diff --git a/coderd/userauth.go b/coderd/userauth.go index b1392d0569b83..2132df20415ef 100644 --- a/coderd/userauth.go +++ b/coderd/userauth.go @@ -261,7 +261,7 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) { // The username is a required property in Coder. We make a best-effort // attempt at using what the claims provide, but if that fails we will // generate a random username. - usernameValid := httpapi.UsernameValid(username) + usernameValid := httpapi.NameValid(username) if usernameValid != nil { // If no username is provided, we can default to use the email address. // This will be converted in the from function below, so it's safe From 5fac8e998d1e5aa2d0a1babf2e4a17aeaafe6e1e Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 9 Nov 2022 10:15:11 +0100 Subject: [PATCH 03/25] Fix: comment --- coderd/httpapi/name.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/httpapi/name.go b/coderd/httpapi/name.go index bb1786930a8f6..ca71ef43d0507 100644 --- a/coderd/httpapi/name.go +++ b/coderd/httpapi/name.go @@ -35,7 +35,7 @@ func UsernameFrom(str string) string { } // NameValid returns whether the input string is a valid name. -// It is a generic validator for any name (user, workspace, etc.). +// It is a generic validator for any name (user, workspace, template, etc.). func NameValid(str string) error { if len(str) > 32 { return xerrors.New("must be <= 32 characters") From 61074d97b75e0b22e6c794b2e22235abf0fbb525 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 9 Nov 2022 10:35:42 +0100 Subject: [PATCH 04/25] Define new migration --- coderd/database/dump.sql | 3 ++- .../000072_template_display_name.down.sql | 1 + .../000072_template_display_name.up.sql | 1 + coderd/database/models.go | 1 + coderd/database/queries.sql.go | 21 ++++++++++++------- coderd/database/queries/templates.sql | 3 ++- 6 files changed, 21 insertions(+), 9 deletions(-) create mode 100644 coderd/database/migrations/000072_template_display_name.down.sql create mode 100644 coderd/database/migrations/000072_template_display_name.up.sql diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 2267fdc108232..f3d761f389b2a 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -354,7 +354,8 @@ CREATE TABLE templates ( created_by uuid NOT NULL, icon character varying(256) DEFAULT ''::character varying NOT NULL, user_acl jsonb DEFAULT '{}'::jsonb NOT NULL, - group_acl jsonb DEFAULT '{}'::jsonb NOT NULL + group_acl jsonb DEFAULT '{}'::jsonb NOT NULL, + display_name character varying(64) DEFAULT ''::character varying NOT NULL ); CREATE TABLE user_links ( diff --git a/coderd/database/migrations/000072_template_display_name.down.sql b/coderd/database/migrations/000072_template_display_name.down.sql new file mode 100644 index 0000000000000..d12e2af3a5462 --- /dev/null +++ b/coderd/database/migrations/000072_template_display_name.down.sql @@ -0,0 +1 @@ +ALTER TABLE templates DROP COLUMN display_name; diff --git a/coderd/database/migrations/000072_template_display_name.up.sql b/coderd/database/migrations/000072_template_display_name.up.sql new file mode 100644 index 0000000000000..5da16e4d468e9 --- /dev/null +++ b/coderd/database/migrations/000072_template_display_name.up.sql @@ -0,0 +1 @@ +ALTER TABLE templates ADD COLUMN display_name VARCHAR(64) NOT NULL DEFAULT ''; diff --git a/coderd/database/models.go b/coderd/database/models.go index bdebbf1a849cb..9d76f126c6942 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -589,6 +589,7 @@ type Template struct { Icon string `db:"icon" json:"icon"` UserACL TemplateACL `db:"user_acl" json:"user_acl"` GroupACL TemplateACL `db:"group_acl" json:"group_acl"` + DisplayName string `db:"display_name" json:"display_name"` } type TemplateVersion struct { diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 473634da9515f..dbd8de1b828cf 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -3019,7 +3019,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, max_ttl, min_autostart_interval, created_by, icon, user_acl, group_acl + id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by, icon, user_acl, group_acl, display_name FROM templates WHERE @@ -3047,13 +3047,14 @@ func (q *sqlQuerier) GetTemplateByID(ctx context.Context, id uuid.UUID) (Templat &i.Icon, &i.UserACL, &i.GroupACL, + &i.DisplayName, ) return i, err } const getTemplateByOrganizationAndName = `-- name: GetTemplateByOrganizationAndName :one SELECT - id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by, icon, user_acl, group_acl + id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by, icon, user_acl, group_acl, display_name FROM templates WHERE @@ -3089,12 +3090,13 @@ func (q *sqlQuerier) GetTemplateByOrganizationAndName(ctx context.Context, arg G &i.Icon, &i.UserACL, &i.GroupACL, + &i.DisplayName, ) return i, err } const getTemplates = `-- name: GetTemplates :many -SELECT id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by, icon, user_acl, group_acl FROM templates +SELECT id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by, icon, user_acl, group_acl, display_name FROM templates ORDER BY (name, id) ASC ` @@ -3123,6 +3125,7 @@ func (q *sqlQuerier) GetTemplates(ctx context.Context) ([]Template, error) { &i.Icon, &i.UserACL, &i.GroupACL, + &i.DisplayName, ); err != nil { return nil, err } @@ -3139,7 +3142,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, max_ttl, min_autostart_interval, created_by, icon, user_acl, group_acl + id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by, icon, user_acl, group_acl, display_name FROM templates WHERE @@ -3203,6 +3206,7 @@ func (q *sqlQuerier) GetTemplatesWithFilter(ctx context.Context, arg GetTemplate &i.Icon, &i.UserACL, &i.GroupACL, + &i.DisplayName, ); err != nil { return nil, err } @@ -3236,7 +3240,7 @@ INSERT INTO group_acl ) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by, icon, user_acl, group_acl + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by, icon, user_acl, group_acl, display_name ` type InsertTemplateParams struct { @@ -3290,6 +3294,7 @@ func (q *sqlQuerier) InsertTemplate(ctx context.Context, arg InsertTemplateParam &i.Icon, &i.UserACL, &i.GroupACL, + &i.DisplayName, ) return i, err } @@ -3303,7 +3308,7 @@ SET WHERE id = $3 RETURNING - id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by, icon, user_acl, group_acl + id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by, icon, user_acl, group_acl, display_name ` type UpdateTemplateACLByIDParams struct { @@ -3331,6 +3336,7 @@ func (q *sqlQuerier) UpdateTemplateACLByID(ctx context.Context, arg UpdateTempla &i.Icon, &i.UserACL, &i.GroupACL, + &i.DisplayName, ) return i, err } @@ -3390,7 +3396,7 @@ SET WHERE id = $1 RETURNING - id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by, icon, user_acl, group_acl + id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by, icon, user_acl, group_acl, display_name ` type UpdateTemplateMetaByIDParams struct { @@ -3430,6 +3436,7 @@ func (q *sqlQuerier) UpdateTemplateMetaByID(ctx context.Context, arg UpdateTempl &i.Icon, &i.UserACL, &i.GroupACL, + &i.DisplayName, ) return i, err } diff --git a/coderd/database/queries/templates.sql b/coderd/database/queries/templates.sql index 6eb73af288b81..35192debfe2eb 100644 --- a/coderd/database/queries/templates.sql +++ b/coderd/database/queries/templates.sql @@ -62,6 +62,7 @@ INSERT INTO updated_at, organization_id, "name", + display_name, provisioner, active_version_id, description, @@ -73,7 +74,7 @@ INSERT INTO group_acl ) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING *; + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) RETURNING *; -- name: UpdateTemplateActiveVersionByID :exec UPDATE From dbd96c081a251bce6ec644da8ef08297155e96b2 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 9 Nov 2022 11:17:16 +0100 Subject: [PATCH 05/25] Include display_name --- coderd/database/queries.sql.go | 12 +++++++++--- coderd/database/queries/templates.sql | 7 ++++--- coderd/httpapi/httpapi.go | 15 +++++++++++++++ coderd/httpapi/name.go | 17 +++++++++++++++++ coderd/templates.go | 7 +++++++ 5 files changed, 52 insertions(+), 6 deletions(-) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index dbd8de1b828cf..9802cf26a85d0 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -3237,10 +3237,11 @@ INSERT INTO created_by, icon, user_acl, - group_acl + group_acl, + display_name ) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by, icon, user_acl, group_acl, display_name + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) RETURNING id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by, icon, user_acl, group_acl, display_name ` type InsertTemplateParams struct { @@ -3258,6 +3259,7 @@ type InsertTemplateParams struct { Icon string `db:"icon" json:"icon"` UserACL TemplateACL `db:"user_acl" json:"user_acl"` GroupACL TemplateACL `db:"group_acl" json:"group_acl"` + DisplayName string `db:"display_name" json:"display_name"` } func (q *sqlQuerier) InsertTemplate(ctx context.Context, arg InsertTemplateParams) (Template, error) { @@ -3276,6 +3278,7 @@ func (q *sqlQuerier) InsertTemplate(ctx context.Context, arg InsertTemplateParam arg.Icon, arg.UserACL, arg.GroupACL, + arg.DisplayName, ) var i Template err := row.Scan( @@ -3392,7 +3395,8 @@ SET max_ttl = $4, min_autostart_interval = $5, name = $6, - icon = $7 + icon = $7, + display_name = $8 WHERE id = $1 RETURNING @@ -3407,6 +3411,7 @@ type UpdateTemplateMetaByIDParams struct { MinAutostartInterval int64 `db:"min_autostart_interval" json:"min_autostart_interval"` Name string `db:"name" json:"name"` Icon string `db:"icon" json:"icon"` + DisplayName string `db:"display_name" json:"display_name"` } func (q *sqlQuerier) UpdateTemplateMetaByID(ctx context.Context, arg UpdateTemplateMetaByIDParams) (Template, error) { @@ -3418,6 +3423,7 @@ func (q *sqlQuerier) UpdateTemplateMetaByID(ctx context.Context, arg UpdateTempl arg.MinAutostartInterval, arg.Name, arg.Icon, + arg.DisplayName, ) var i Template err := row.Scan( diff --git a/coderd/database/queries/templates.sql b/coderd/database/queries/templates.sql index 35192debfe2eb..b517f4d76fe97 100644 --- a/coderd/database/queries/templates.sql +++ b/coderd/database/queries/templates.sql @@ -62,7 +62,6 @@ INSERT INTO updated_at, organization_id, "name", - display_name, provisioner, active_version_id, description, @@ -71,7 +70,8 @@ INSERT INTO created_by, icon, user_acl, - group_acl + group_acl, + display_name ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) RETURNING *; @@ -103,7 +103,8 @@ SET max_ttl = $4, min_autostart_interval = $5, name = $6, - icon = $7 + icon = $7, + display_name = $8 WHERE id = $1 RETURNING diff --git a/coderd/httpapi/httpapi.go b/coderd/httpapi/httpapi.go index 091eae0f883f9..fb55cbb9ff5c7 100644 --- a/coderd/httpapi/httpapi.go +++ b/coderd/httpapi/httpapi.go @@ -33,6 +33,7 @@ func init() { } return name }) + nameValidator := func(fl validator.FieldLevel) bool { f := fl.Field().Interface() str, ok := f.(string) @@ -48,6 +49,20 @@ func init() { panic(err) } } + + templateDisplayNameValidator := func(fl validator.FieldLevel) bool { + f := fl.Field().Interface() + str, ok := f.(string) + if !ok { + return false + } + valid := TemplateDisplayNameValid(str) + return valid == nil + } + err := validate.RegisterValidation("template_display_name", templateDisplayNameValidator) + if err != nil { + panic(err) + } } // Convenience error functions don't take contexts since their responses are diff --git a/coderd/httpapi/name.go b/coderd/httpapi/name.go index ca71ef43d0507..9d216f4abb58a 100644 --- a/coderd/httpapi/name.go +++ b/coderd/httpapi/name.go @@ -11,6 +11,8 @@ import ( var ( UsernameValidRegex = regexp.MustCompile("^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$") usernameReplace = regexp.MustCompile("[^a-zA-Z0-9-]*") + + templateDisplayName = regexp.MustCompile("^[a-zA-Z0-9]+(?: [a-zA-Z0-9]+)*$") ) // UsernameFrom returns a best-effort username from the provided string. @@ -49,3 +51,18 @@ func NameValid(str string) error { } return nil } + +// TemplateDisplayNameValid returns whether the input string is a valid template display name. +func TemplateDisplayNameValid(str string) error { + if len(str) > 32 { + return xerrors.New("must be <= 32 characters") + } + if len(str) < 1 { + return xerrors.New("must be >= 1 character") + } + matched := templateDisplayName.MatchString(str) + if !matched { + return xerrors.New("must be alphanumeric with spaces") + } + return nil +} diff --git a/coderd/templates.go b/coderd/templates.go index 163464db39b12..7bafa5224d885 100644 --- a/coderd/templates.go +++ b/coderd/templates.go @@ -500,6 +500,7 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) { if req.Name == template.Name && req.Description == template.Description && + req.DisplayName == template.DisplayName && req.Icon == template.Icon && req.MaxTTLMillis == time.Duration(template.MaxTtl).Milliseconds() && req.MinAutostartIntervalMillis == time.Duration(template.MinAutostartInterval).Milliseconds() { @@ -508,6 +509,7 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) { // Update template metadata -- empty fields are not overwritten. name := req.Name + displayName := req.DisplayName desc := req.Description icon := req.Icon maxTTL := time.Duration(req.MaxTTLMillis) * time.Millisecond @@ -516,6 +518,9 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) { if name == "" { name = template.Name } + if displayName == "" { + displayName = template.DisplayName + } if desc == "" { desc = template.Description } @@ -527,6 +532,7 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) { ID: template.ID, UpdatedAt: database.Now(), Name: name, + DisplayName: displayName, Description: desc, Icon: icon, MaxTtl: int64(maxTTL), @@ -773,6 +779,7 @@ func (api *API) convertTemplate( UpdatedAt: template.UpdatedAt, OrganizationID: template.OrganizationID, Name: template.Name, + DisplayName: template.DisplayName, Provisioner: codersdk.ProvisionerType(template.Provisioner), ActiveVersionID: template.ActiveVersionID, WorkspaceOwnerCount: workspaceOwnerCount, From bf26ba99fb7420f1a1576294172b8f0d9fdc903f Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 9 Nov 2022 11:17:38 +0100 Subject: [PATCH 06/25] Update typesGenerated.ts --- site/src/api/typesGenerated.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index b5b9297a61e25..e6457a0ade2c1 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -613,6 +613,7 @@ export interface Template { readonly updated_at: string readonly organization_id: string readonly name: string + readonly display_name: string readonly provisioner: ProvisionerType readonly active_version_id: string readonly workspace_owner_count: number @@ -697,6 +698,7 @@ export interface UpdateTemplateACL { // From codersdk/templates.go export interface UpdateTemplateMeta { readonly name?: string + readonly display_name?: string readonly description?: string readonly icon?: string readonly max_ttl_ms?: number From 9a7d35f6dc97fdf5e8d280aa2c6fcdaf657e9673 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 9 Nov 2022 11:24:16 +0100 Subject: [PATCH 07/25] Update meta --- codersdk/templates.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/codersdk/templates.go b/codersdk/templates.go index 22e707050aed3..049498063edc8 100644 --- a/codersdk/templates.go +++ b/codersdk/templates.go @@ -19,6 +19,7 @@ type Template struct { UpdatedAt time.Time `json:"updated_at"` OrganizationID uuid.UUID `json:"organization_id"` Name string `json:"name"` + DisplayName string `json:"display_name"` Provisioner ProvisionerType `json:"provisioner"` ActiveVersionID uuid.UUID `json:"active_version_id"` WorkspaceOwnerCount uint32 `json:"workspace_owner_count"` @@ -72,7 +73,8 @@ type UpdateTemplateACL struct { } type UpdateTemplateMeta struct { - Name string `json:"name,omitempty" validate:"omitempty,username"` + Name string `json:"name,omitempty" validate:"omitempty,template_name"` + DisplayName string `json:"display_name,omitempty" validate:"omitempty,template_display_name"` Description string `json:"description,omitempty"` Icon string `json:"icon,omitempty"` MaxTTLMillis int64 `json:"max_ttl_ms,omitempty"` From 4aaff3e399139a879f4a0dcd87531916d45184cb Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 9 Nov 2022 12:23:21 +0100 Subject: [PATCH 08/25] Adjust tests --- coderd/database/databasefake/databasefake.go | 1 + coderd/httpapi/name.go | 6 +++--- coderd/templates_test.go | 10 ++++------ codersdk/organizations.go | 2 ++ 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/coderd/database/databasefake/databasefake.go b/coderd/database/databasefake/databasefake.go index db9719c11f4cf..6dc353b317e2c 100644 --- a/coderd/database/databasefake/databasefake.go +++ b/coderd/database/databasefake/databasefake.go @@ -1452,6 +1452,7 @@ func (q *fakeQuerier) UpdateTemplateMetaByID(_ context.Context, arg database.Upd } tpl.UpdatedAt = database.Now() tpl.Name = arg.Name + tpl.DisplayName = arg.DisplayName tpl.Description = arg.Description tpl.Icon = arg.Icon tpl.MaxTtl = arg.MaxTtl diff --git a/coderd/httpapi/name.go b/coderd/httpapi/name.go index 9d216f4abb58a..7386d35b3db8a 100644 --- a/coderd/httpapi/name.go +++ b/coderd/httpapi/name.go @@ -54,12 +54,12 @@ func NameValid(str string) error { // TemplateDisplayNameValid returns whether the input string is a valid template display name. func TemplateDisplayNameValid(str string) error { + if len(str) == 0 { + return nil // empty display_name is correct + } if len(str) > 32 { return xerrors.New("must be <= 32 characters") } - if len(str) < 1 { - return xerrors.New("must be >= 1 character") - } matched := templateDisplayName.MatchString(str) if !matched { return xerrors.New("must be alphanumeric with spaces") diff --git a/coderd/templates_test.go b/coderd/templates_test.go index 787bec3db0d2f..e7709cdd43c39 100644 --- a/coderd/templates_test.go +++ b/coderd/templates_test.go @@ -303,14 +303,10 @@ func TestPatchTemplateMeta(t *testing.T) { client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, Auditor: auditor}) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) - template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { - ctr.Description = "original description" - ctr.Icon = "/icons/original-icon.png" - ctr.MaxTTLMillis = ptr.Ref(24 * time.Hour.Milliseconds()) - ctr.MinAutostartIntervalMillis = ptr.Ref(time.Hour.Milliseconds()) - }) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) req := codersdk.UpdateTemplateMeta{ Name: "new-template-name", + DisplayName: "Displayed Name 456", Description: "lorem ipsum dolor sit amet et cetera", Icon: "/icons/new-icon.png", MaxTTLMillis: 12 * time.Hour.Milliseconds(), @@ -327,6 +323,7 @@ func TestPatchTemplateMeta(t *testing.T) { require.NoError(t, err) assert.Greater(t, updated.UpdatedAt, template.UpdatedAt) assert.Equal(t, req.Name, updated.Name) + assert.Equal(t, req.DisplayName, updated.DisplayName) assert.Equal(t, req.Description, updated.Description) assert.Equal(t, req.Icon, updated.Icon) assert.Equal(t, req.MaxTTLMillis, updated.MaxTTLMillis) @@ -337,6 +334,7 @@ func TestPatchTemplateMeta(t *testing.T) { require.NoError(t, err) assert.Greater(t, updated.UpdatedAt, template.UpdatedAt) assert.Equal(t, req.Name, updated.Name) + assert.Equal(t, req.DisplayName, updated.DisplayName) assert.Equal(t, req.Description, updated.Description) assert.Equal(t, req.Icon, updated.Icon) assert.Equal(t, req.MaxTTLMillis, updated.MaxTTLMillis) diff --git a/codersdk/organizations.go b/codersdk/organizations.go index de5e42122ce28..54f70f7195404 100644 --- a/codersdk/organizations.go +++ b/codersdk/organizations.go @@ -50,6 +50,8 @@ type CreateTemplateVersionRequest struct { type CreateTemplateRequest struct { // Name is the name of the template. Name string `json:"name" validate:"template_name,required"` + // DisplayName is the displayed name of the template. + DisplayName string `json:"display_name" validate:"template_display_name"` // Description is a description of what the template contains. It must be // less than 128 bytes. Description string `json:"description,omitempty" validate:"lt=128"` From c3532a1256e3c9bac6072b8cca519ed7cde30649 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 9 Nov 2022 13:05:36 +0100 Subject: [PATCH 09/25] CLI tests --- cli/templateedit.go | 3 +++ cli/templateedit_test.go | 49 ++++++++++++++++++++++++++++------------ 2 files changed, 37 insertions(+), 15 deletions(-) diff --git a/cli/templateedit.go b/cli/templateedit.go index e0e4cf57a7196..b93b557d4b56b 100644 --- a/cli/templateedit.go +++ b/cli/templateedit.go @@ -14,6 +14,7 @@ import ( func templateEdit() *cobra.Command { var ( name string + displayName string description string icon string maxTTL time.Duration @@ -41,6 +42,7 @@ func templateEdit() *cobra.Command { // NOTE: coderd will ignore empty fields. req := codersdk.UpdateTemplateMeta{ Name: name, + DisplayName: displayName, Description: description, Icon: icon, MaxTTLMillis: maxTTL.Milliseconds(), @@ -57,6 +59,7 @@ func templateEdit() *cobra.Command { } cmd.Flags().StringVarP(&name, "name", "", "", "Edit the template name") + cmd.Flags().StringVarP(&displayName, "display-name", "", "", "Edit the template display name") cmd.Flags().StringVarP(&description, "description", "", "", "Edit the template description") cmd.Flags().StringVarP(&icon, "icon", "", "", "Edit the template icon path") cmd.Flags().DurationVarP(&maxTTL, "max-ttl", "", 0, "Edit the template maximum time before shutdown - workspaces created from this template cannot stay running longer than this.") diff --git a/cli/templateedit_test.go b/cli/templateedit_test.go index 61437764021f6..e37c140b30655 100644 --- a/cli/templateedit_test.go +++ b/cli/templateedit_test.go @@ -10,8 +10,6 @@ import ( "github.com/coder/coder/cli/clitest" "github.com/coder/coder/coderd/coderdtest" - "github.com/coder/coder/coderd/util/ptr" - "github.com/coder/coder/codersdk" ) func TestTemplateEdit(t *testing.T) { @@ -23,15 +21,11 @@ func TestTemplateEdit(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) _ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID) - template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { - ctr.Description = "original description" - ctr.Icon = "/icons/default-icon.png" - ctr.MaxTTLMillis = ptr.Ref(24 * time.Hour.Milliseconds()) - ctr.MinAutostartIntervalMillis = ptr.Ref(time.Hour.Milliseconds()) - }) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) // Test the cli command. name := "new-template-name" + displayName := "New Display Name 789" desc := "lorem ipsum dolor sit amet et cetera" icon := "/icons/new-icon.png" maxTTL := 12 * time.Hour @@ -41,6 +35,7 @@ func TestTemplateEdit(t *testing.T) { "edit", template.Name, "--name", name, + "--display-name", displayName, "--description", desc, "--icon", icon, "--max-ttl", maxTTL.String(), @@ -57,24 +52,19 @@ func TestTemplateEdit(t *testing.T) { updated, err := client.Template(context.Background(), template.ID) require.NoError(t, err) assert.Equal(t, name, updated.Name) + assert.Equal(t, displayName, updated.DisplayName) assert.Equal(t, desc, updated.Description) assert.Equal(t, icon, updated.Icon) assert.Equal(t, maxTTL.Milliseconds(), updated.MaxTTLMillis) assert.Equal(t, minAutostartInterval.Milliseconds(), updated.MinAutostartIntervalMillis) }) - t.Run("NotModified", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) _ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID) - template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { - ctr.Description = "original description" - ctr.Icon = "/icons/default-icon.png" - ctr.MaxTTLMillis = ptr.Ref(24 * time.Hour.Milliseconds()) - ctr.MinAutostartIntervalMillis = ptr.Ref(time.Hour.Milliseconds()) - }) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) // Test the cli command. cmdArgs := []string{ @@ -103,4 +93,33 @@ func TestTemplateEdit(t *testing.T) { assert.Equal(t, template.MaxTTLMillis, updated.MaxTTLMillis) assert.Equal(t, template.MinAutostartIntervalMillis, updated.MinAutostartIntervalMillis) }) + t.Run("InvalidDisplayName", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + _ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + + // Test the cli command. + cmdArgs := []string{ + "templates", + "edit", + template.Name, + "--name", template.Name, + "--display-name", "a-b-c", + } + cmd, root := clitest.New(t, cmdArgs...) + clitest.SetupConfig(t, client, root) + + err := cmd.Execute() + + require.ErrorContains(t, err, `Validation failed for tag "template_display_name"`) + + // Assert that the template metadata did not change. + updated, err := client.Template(context.Background(), template.ID) + require.NoError(t, err) + assert.Equal(t, template.Name, updated.Name) + assert.Equal(t, "", template.DisplayName) + }) } From fa778d31b2f5fe9a2c4acfa19a59b535e2154045 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 9 Nov 2022 13:17:41 +0100 Subject: [PATCH 10/25] Fix: audit --- enterprise/audit/table.go | 1 + 1 file changed, 1 insertion(+) diff --git a/enterprise/audit/table.go b/enterprise/audit/table.go index 2578ae7437844..1b705a4d8ad66 100644 --- a/enterprise/audit/table.go +++ b/enterprise/audit/table.go @@ -54,6 +54,7 @@ var AuditableResources = auditMap(map[any]map[string]Action{ "organization_id": ActionIgnore, /// Never changes. "deleted": ActionIgnore, // Changes, but is implicit when a delete event is fired. "name": ActionTrack, + "display_name": ActionTrack, "provisioner": ActionTrack, "active_version_id": ActionTrack, "description": ActionTrack, From 64c6fe2d7803740206f66a7d71b28ec81ce6524a Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 9 Nov 2022 13:26:00 +0100 Subject: [PATCH 11/25] Fix: omitempty --- codersdk/organizations.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codersdk/organizations.go b/codersdk/organizations.go index 54f70f7195404..ecfcf3ec88078 100644 --- a/codersdk/organizations.go +++ b/codersdk/organizations.go @@ -51,7 +51,7 @@ type CreateTemplateRequest struct { // Name is the name of the template. Name string `json:"name" validate:"template_name,required"` // DisplayName is the displayed name of the template. - DisplayName string `json:"display_name" validate:"template_display_name"` + DisplayName string `json:"display_name,omitempty" validate:"template_display_name"` // Description is a description of what the template contains. It must be // less than 128 bytes. Description string `json:"description,omitempty" validate:"lt=128"` From b2c122b6d5311565068faea136e3a328b5c5be16 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 9 Nov 2022 13:26:14 +0100 Subject: [PATCH 12/25] site: display_name is optional --- site/src/api/typesGenerated.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index e6457a0ade2c1..fae8e1901f96b 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -175,6 +175,7 @@ export interface CreateParameterRequest { // From codersdk/organizations.go export interface CreateTemplateRequest { readonly name: string + readonly display_name?: string readonly description?: string readonly icon?: string readonly template_version_id: string From 75c79d3e2d910e460b6b25daf33a029d80719e86 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 9 Nov 2022 13:32:22 +0100 Subject: [PATCH 13/25] unit: TestUsernameValid --- coderd/httpapi/name_test.go | 59 ++++++++++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/coderd/httpapi/name_test.go b/coderd/httpapi/name_test.go index de79261457a9e..03b47d4cd9074 100644 --- a/coderd/httpapi/name_test.go +++ b/coderd/httpapi/name_test.go @@ -8,7 +8,7 @@ import ( "github.com/coder/coder/coderd/httpapi" ) -func TestValid(t *testing.T) { +func TestUsernameValid(t *testing.T) { t.Parallel() // Tests whether usernames are valid or not. testCases := []struct { @@ -65,6 +65,63 @@ func TestValid(t *testing.T) { } } +func TestTemplateDisplayNameValid(t *testing.T) { + t.Parallel() + // Tests whether display names are valid. + testCases := []struct { + Username string + Valid bool + }{ + {"", true}, + {"1", true}, + {"12", true}, + {"1 2", true}, + {"1234 678901234567890", true}, + {"1234567890123 5678901", true}, + {"S", true}, + {"a1", true}, + {"a1K2", true}, + {"a1b2c3 4e5f6g7h8i9j0", true}, + {"a1b2c3d4e5f6g h8i9j0k", true}, + {"aa", true}, + {"aNc", true}, + {"abcdefghijklmnopqrst", true}, + {"abcdefghijklmnopqrstu", true}, + {"Wow Test", true}, + + {" ", false}, + {" a", false}, + {" a ", false}, + {" 1", false}, + {"1 ", false}, + {" aa", false}, + {"aa ", false}, + {" 12", false}, + {"12 ", false}, + {" a1", false}, + {"a1 ", false}, + {"-abcdefghijKLmnopqrstu", false}, + {"abcdefghijklmnopqrstu-", false}, + {"-123456789012345678901", false}, + {"-a1b2c3d4e5f6g7h8i9j0k", false}, + {"a1b2c3d4e5f6g7h8i9j0k-", false}, + {"BANANAS_wow", false}, + {"test--now", false}, + + {"123456789012345678901234567890123", false}, + {"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", false}, + {"123456789012345678901234567890123123456789012345678901234567890123", false}, + } + for _, testCase := range testCases { + testCase := testCase + t.Run(testCase.Username, func(t *testing.T) { + t.Parallel() + valid := httpapi.TemplateDisplayNameValid(testCase.Username) + require.Equal(t, testCase.Valid, valid == nil) + }) + } +} + func TestFrom(t *testing.T) { t.Parallel() testCases := []struct { From cfa4ca7e671ed39691a6475e3de1a4a0dc121779 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 9 Nov 2022 14:24:32 +0100 Subject: [PATCH 14/25] entities.ts: add display_name --- site/src/testHelpers/entities.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 8e7c27951b9ca..45f16190a07c7 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -188,6 +188,7 @@ export const MockTemplate: TypesGen.Template = { updated_at: "2022-05-18T17:39:01.382927298Z", organization_id: MockOrganization.id, name: "test-template", + display_name: "Test Template", provisioner: MockProvisioner.provisioners[0], active_version_id: MockTemplateVersion.id, workspace_owner_count: 2, From 9eb443edb3b18160d842ada0b103dff2451e9ca4 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 9 Nov 2022 14:33:32 +0100 Subject: [PATCH 15/25] site: TemplateSettingsPage.test.tsx --- .../src/pages/TemplateSettingsPage/TemplateSettingsPage.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/site/src/pages/TemplateSettingsPage/TemplateSettingsPage.test.tsx b/site/src/pages/TemplateSettingsPage/TemplateSettingsPage.test.tsx index f6db0bde004d9..5917b36624365 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSettingsPage.test.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSettingsPage.test.tsx @@ -24,6 +24,7 @@ const renderTemplateSettingsPage = async () => { const validFormValues = { name: "Name", + display_name: "Display Name", description: "A description", icon: "A string", max_ttl_ms: 1, From 20018f82b880f1df5860119b6bea0aa4f01fc36c Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 9 Nov 2022 14:56:31 +0100 Subject: [PATCH 16/25] Fix: TemplateSettingsForm.tsx --- site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx | 1 + .../pages/TemplateSettingsPage/TemplateSettingsPage.test.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx index 7ef9f758fc498..040db196f592f 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx @@ -70,6 +70,7 @@ export const TemplateSettingsForm: FC = ({ useFormik({ initialValues: { name: template.name, + display_name: template.display_name, description: template.description, // on display, convert from ms => hours max_ttl_ms: template.max_ttl_ms / MS_HOUR_CONVERSION, diff --git a/site/src/pages/TemplateSettingsPage/TemplateSettingsPage.test.tsx b/site/src/pages/TemplateSettingsPage/TemplateSettingsPage.test.tsx index 5917b36624365..f5702c3a7426b 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSettingsPage.test.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSettingsPage.test.tsx @@ -24,7 +24,7 @@ const renderTemplateSettingsPage = async () => { const validFormValues = { name: "Name", - display_name: "Display Name", + display_name: "Test Template", description: "A description", icon: "A string", max_ttl_ms: 1, From 53c463dc286980dc366d432940e744aa58533f84 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 9 Nov 2022 17:52:32 +0100 Subject: [PATCH 17/25] Adjust tests --- cli/templateedit_test.go | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/cli/templateedit_test.go b/cli/templateedit_test.go index e37c140b30655..584e4a5f79185 100644 --- a/cli/templateedit_test.go +++ b/cli/templateedit_test.go @@ -10,6 +10,8 @@ import ( "github.com/coder/coder/cli/clitest" "github.com/coder/coder/coderd/coderdtest" + "github.com/coder/coder/codersdk" + "github.com/coder/coder/testutil" ) func TestTemplateEdit(t *testing.T) { @@ -44,7 +46,8 @@ func TestTemplateEdit(t *testing.T) { cmd, root := clitest.New(t, cmdArgs...) clitest.SetupConfig(t, client, root) - err := cmd.Execute() + ctx, _ := testutil.Context(t) + err := cmd.ExecuteContext(ctx) require.NoError(t, err) @@ -80,7 +83,8 @@ func TestTemplateEdit(t *testing.T) { cmd, root := clitest.New(t, cmdArgs...) clitest.SetupConfig(t, client, root) - err := cmd.Execute() + ctx, _ := testutil.Context(t) + err := cmd.ExecuteContext(ctx) require.ErrorContains(t, err, "not modified") @@ -112,9 +116,14 @@ func TestTemplateEdit(t *testing.T) { cmd, root := clitest.New(t, cmdArgs...) clitest.SetupConfig(t, client, root) - err := cmd.Execute() + ctx, _ := testutil.Context(t) + err := cmd.ExecuteContext(ctx) - require.ErrorContains(t, err, `Validation failed for tag "template_display_name"`) + require.Error(t, err, "client call must fail") + sdkError, isSdkError := codersdk.AsError(err) + require.True(t, isSdkError, "sdk error is expected") + require.Len(t, sdkError.Response.Validations, 1, "field validation error is expected") + require.Equal(t, sdkError.Response.Validations[0].Detail, `Validation failed for tag "template_display_name" with value: "a-b-c"`) // Assert that the template metadata did not change. updated, err := client.Template(context.Background(), template.ID) From a24eac4bdc82204009ffdea6a248d02327725386 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 9 Nov 2022 18:08:22 +0100 Subject: [PATCH 18/25] Add comment to display_name column --- coderd/database/migrations/000072_template_display_name.up.sql | 3 +++ 1 file changed, 3 insertions(+) diff --git a/coderd/database/migrations/000072_template_display_name.up.sql b/coderd/database/migrations/000072_template_display_name.up.sql index 5da16e4d468e9..9c44347cc6789 100644 --- a/coderd/database/migrations/000072_template_display_name.up.sql +++ b/coderd/database/migrations/000072_template_display_name.up.sql @@ -1 +1,4 @@ ALTER TABLE templates ADD COLUMN display_name VARCHAR(64) NOT NULL DEFAULT ''; + +COMMENT ON COLUMN templates.display_name +IS 'Display name is a custom, human-friendly template name that user can set.'; From 7490d453b80974853517e4a8de5c60283fae7a7b Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 9 Nov 2022 18:10:02 +0100 Subject: [PATCH 19/25] Fix: rename --- ...isplay_name.down.sql => 000073_template_display_name.down.sql} | 0 ...te_display_name.up.sql => 000073_template_display_name.up.sql} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename coderd/database/migrations/{000072_template_display_name.down.sql => 000073_template_display_name.down.sql} (100%) rename coderd/database/migrations/{000072_template_display_name.up.sql => 000073_template_display_name.up.sql} (100%) diff --git a/coderd/database/migrations/000072_template_display_name.down.sql b/coderd/database/migrations/000073_template_display_name.down.sql similarity index 100% rename from coderd/database/migrations/000072_template_display_name.down.sql rename to coderd/database/migrations/000073_template_display_name.down.sql diff --git a/coderd/database/migrations/000072_template_display_name.up.sql b/coderd/database/migrations/000073_template_display_name.up.sql similarity index 100% rename from coderd/database/migrations/000072_template_display_name.up.sql rename to coderd/database/migrations/000073_template_display_name.up.sql From 88c6ddc2dfd52713f0aaee167cf2e8fa0404773e Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 9 Nov 2022 18:13:22 +0100 Subject: [PATCH 20/25] Fix: make --- coderd/database/dump.sql | 2 ++ coderd/database/models.go | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 9164d5c87ad27..c601d6dd16211 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -358,6 +358,8 @@ CREATE TABLE templates ( display_name character varying(64) DEFAULT ''::character varying NOT NULL ); +COMMENT ON COLUMN templates.display_name IS 'Display name is a custom, human-friendly template name that user can set.'; + CREATE TABLE user_links ( user_id uuid NOT NULL, login_type login_type NOT NULL, diff --git a/coderd/database/models.go b/coderd/database/models.go index a4890bcd176b4..8f4ec48d6837c 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -589,7 +589,8 @@ type Template struct { Icon string `db:"icon" json:"icon"` UserACL TemplateACL `db:"user_acl" json:"user_acl"` GroupACL TemplateACL `db:"group_acl" json:"group_acl"` - DisplayName string `db:"display_name" json:"display_name"` + // Display name is a custom, human-friendly template name that user can set. + DisplayName string `db:"display_name" json:"display_name"` } type TemplateVersion struct { From 361a4281fd16bc994cf58bc61d6cbe35c7d27c51 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 10 Nov 2022 12:15:15 +0100 Subject: [PATCH 21/25] Loosen regexp --- coderd/httpapi/name.go | 2 +- coderd/httpapi/name_test.go | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/coderd/httpapi/name.go b/coderd/httpapi/name.go index 7386d35b3db8a..d7fe17f64b045 100644 --- a/coderd/httpapi/name.go +++ b/coderd/httpapi/name.go @@ -12,7 +12,7 @@ var ( UsernameValidRegex = regexp.MustCompile("^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$") usernameReplace = regexp.MustCompile("[^a-zA-Z0-9-]*") - templateDisplayName = regexp.MustCompile("^[a-zA-Z0-9]+(?: [a-zA-Z0-9]+)*$") + templateDisplayName = regexp.MustCompile("^[a-zA-Z0-9](.*[^ ])?$") ) // UsernameFrom returns a best-effort username from the provided string. diff --git a/coderd/httpapi/name_test.go b/coderd/httpapi/name_test.go index 03b47d4cd9074..18519c1b993f0 100644 --- a/coderd/httpapi/name_test.go +++ b/coderd/httpapi/name_test.go @@ -69,8 +69,8 @@ func TestTemplateDisplayNameValid(t *testing.T) { t.Parallel() // Tests whether display names are valid. testCases := []struct { - Username string - Valid bool + Name string + Valid bool }{ {"", true}, {"1", true}, @@ -88,6 +88,10 @@ func TestTemplateDisplayNameValid(t *testing.T) { {"abcdefghijklmnopqrst", true}, {"abcdefghijklmnopqrstu", true}, {"Wow Test", true}, + {"abcdefghijklmnopqrstu-", true}, + {"a1b2c3d4e5f6g7h8i9j0k-", true}, + {"BANANAS_wow", true}, + {"test--now", true}, {" ", false}, {" a", false}, @@ -100,13 +104,9 @@ func TestTemplateDisplayNameValid(t *testing.T) { {"12 ", false}, {" a1", false}, {"a1 ", false}, - {"-abcdefghijKLmnopqrstu", false}, - {"abcdefghijklmnopqrstu-", false}, {"-123456789012345678901", false}, {"-a1b2c3d4e5f6g7h8i9j0k", false}, - {"a1b2c3d4e5f6g7h8i9j0k-", false}, - {"BANANAS_wow", false}, - {"test--now", false}, + {"-abcdefghijKLmnopqrstu", false}, {"123456789012345678901234567890123", false}, {"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", false}, @@ -114,9 +114,9 @@ func TestTemplateDisplayNameValid(t *testing.T) { } for _, testCase := range testCases { testCase := testCase - t.Run(testCase.Username, func(t *testing.T) { + t.Run(testCase.Name, func(t *testing.T) { t.Parallel() - valid := httpapi.TemplateDisplayNameValid(testCase.Username) + valid := httpapi.TemplateDisplayNameValid(testCase.Name) require.Equal(t, testCase.Valid, valid == nil) }) } From d935a41d1731995e481bcb2dffed0c8733573f3c Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 10 Nov 2022 12:23:23 +0100 Subject: [PATCH 22/25] Fix: err check --- cli/templateedit_test.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/cli/templateedit_test.go b/cli/templateedit_test.go index 7c2120d5eaba5..ae39b71b4e00b 100644 --- a/cli/templateedit_test.go +++ b/cli/templateedit_test.go @@ -106,7 +106,7 @@ func TestTemplateEdit(t *testing.T) { "edit", template.Name, "--name", template.Name, - "--display-name", "a-b-c", + "--display-name", " a-b-c", } cmd, root := clitest.New(t, cmdArgs...) clitest.SetupConfig(t, client, root) @@ -115,10 +115,8 @@ func TestTemplateEdit(t *testing.T) { err := cmd.ExecuteContext(ctx) require.Error(t, err, "client call must fail") - sdkError, isSdkError := codersdk.AsError(err) + _, isSdkError := codersdk.AsError(err) require.True(t, isSdkError, "sdk error is expected") - require.Len(t, sdkError.Response.Validations, 1, "field validation error is expected") - require.Equal(t, sdkError.Response.Validations[0].Detail, `Validation failed for tag "template_display_name" with value: "a-b-c"`) // Assert that the template metadata did not change. updated, err := client.Template(context.Background(), template.ID) From a837fae418b842f01253c757e0532a3a3e6303fc Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 10 Nov 2022 12:31:54 +0100 Subject: [PATCH 23/25] Fix: template name length --- coderd/httpapi/name.go | 4 ++-- coderd/httpapi/name_test.go | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/coderd/httpapi/name.go b/coderd/httpapi/name.go index d7fe17f64b045..13aa665493e78 100644 --- a/coderd/httpapi/name.go +++ b/coderd/httpapi/name.go @@ -57,8 +57,8 @@ func TemplateDisplayNameValid(str string) error { if len(str) == 0 { return nil // empty display_name is correct } - if len(str) > 32 { - return xerrors.New("must be <= 32 characters") + if len(str) > 64 { + return xerrors.New("must be <= 64 characters") } matched := templateDisplayName.MatchString(str) if !matched { diff --git a/coderd/httpapi/name_test.go b/coderd/httpapi/name_test.go index 18519c1b993f0..905a99e717a6c 100644 --- a/coderd/httpapi/name_test.go +++ b/coderd/httpapi/name_test.go @@ -92,6 +92,9 @@ func TestTemplateDisplayNameValid(t *testing.T) { {"a1b2c3d4e5f6g7h8i9j0k-", true}, {"BANANAS_wow", true}, {"test--now", true}, + {"123456789012345678901234567890123", true}, + {"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", true}, + {"1234567890123456789012345678901234567890123456789012345678901234", true}, {" ", false}, {" a", false}, @@ -107,10 +110,7 @@ func TestTemplateDisplayNameValid(t *testing.T) { {"-123456789012345678901", false}, {"-a1b2c3d4e5f6g7h8i9j0k", false}, {"-abcdefghijKLmnopqrstu", false}, - - {"123456789012345678901234567890123", false}, - {"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", false}, - {"123456789012345678901234567890123123456789012345678901234567890123", false}, + {"12345678901234567890123456789012345678901234567890123456789012345", false}, } for _, testCase := range testCases { testCase := testCase From 2da01471cf08604e56650f47d67e78574d426c57 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 10 Nov 2022 18:39:50 +0100 Subject: [PATCH 24/25] Allow for whitespaces --- coderd/httpapi/name.go | 2 +- coderd/httpapi/name_test.go | 26 ++++++++++++-------------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/coderd/httpapi/name.go b/coderd/httpapi/name.go index 13aa665493e78..9327eb5556596 100644 --- a/coderd/httpapi/name.go +++ b/coderd/httpapi/name.go @@ -12,7 +12,7 @@ var ( UsernameValidRegex = regexp.MustCompile("^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$") usernameReplace = regexp.MustCompile("[^a-zA-Z0-9-]*") - templateDisplayName = regexp.MustCompile("^[a-zA-Z0-9](.*[^ ])?$") + templateDisplayName = regexp.MustCompile(`^[^\s](.*[^\s])?$`) ) // UsernameFrom returns a best-effort username from the provided string. diff --git a/coderd/httpapi/name_test.go b/coderd/httpapi/name_test.go index 905a99e717a6c..d07cce59f1a16 100644 --- a/coderd/httpapi/name_test.go +++ b/coderd/httpapi/name_test.go @@ -76,40 +76,38 @@ func TestTemplateDisplayNameValid(t *testing.T) { {"1", true}, {"12", true}, {"1 2", true}, + {"123 456", true}, {"1234 678901234567890", true}, - {"1234567890123 5678901", true}, + {" ", true}, {"S", true}, {"a1", true}, {"a1K2", true}, - {"a1b2c3 4e5f6g7h8i9j0", true}, - {"a1b2c3d4e5f6g h8i9j0k", true}, - {"aa", true}, - {"aNc", true}, + {"!!!!1 ?????", true}, + {"k\r\rm", true}, {"abcdefghijklmnopqrst", true}, - {"abcdefghijklmnopqrstu", true}, {"Wow Test", true}, {"abcdefghijklmnopqrstu-", true}, {"a1b2c3d4e5f6g7h8i9j0k-", true}, {"BANANAS_wow", true}, {"test--now", true}, {"123456789012345678901234567890123", true}, - {"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", true}, {"1234567890123456789012345678901234567890123456789012345678901234", true}, + {"-a1b2c3d4e5f6g7h8i9j0k", true}, {" ", false}, + {"\t", false}, + {"\r\r", false}, + {"\t1 ", false}, {" a", false}, - {" a ", false}, + {"\ra ", false}, {" 1", false}, {"1 ", false}, {" aa", false}, - {"aa ", false}, + {"aa\r", false}, {" 12", false}, {"12 ", false}, - {" a1", false}, - {"a1 ", false}, - {"-123456789012345678901", false}, - {"-a1b2c3d4e5f6g7h8i9j0k", false}, - {"-abcdefghijKLmnopqrstu", false}, + {"\fa1", false}, + {"a1\t", false}, {"12345678901234567890123456789012345678901234567890123456789012345", false}, } for _, testCase := range testCases { From a3652b27a6277c28798757972e468745c5866c61 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 10 Nov 2022 21:18:43 +0100 Subject: [PATCH 25/25] Update migration number --- ...isplay_name.down.sql => 000075_template_display_name.down.sql} | 0 ...te_display_name.up.sql => 000075_template_display_name.up.sql} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename coderd/database/migrations/{000074_template_display_name.down.sql => 000075_template_display_name.down.sql} (100%) rename coderd/database/migrations/{000074_template_display_name.up.sql => 000075_template_display_name.up.sql} (100%) diff --git a/coderd/database/migrations/000074_template_display_name.down.sql b/coderd/database/migrations/000075_template_display_name.down.sql similarity index 100% rename from coderd/database/migrations/000074_template_display_name.down.sql rename to coderd/database/migrations/000075_template_display_name.down.sql diff --git a/coderd/database/migrations/000074_template_display_name.up.sql b/coderd/database/migrations/000075_template_display_name.up.sql similarity index 100% rename from coderd/database/migrations/000074_template_display_name.up.sql rename to coderd/database/migrations/000075_template_display_name.up.sql