diff --git a/cli/templateedit.go b/cli/templateedit.go
index 867cb41d208a7..50307ec73c819 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
defaultTTL time.Duration
@@ -40,6 +41,7 @@ func templateEdit() *cobra.Command {
// NOTE: coderd will ignore empty fields.
req := codersdk.UpdateTemplateMeta{
Name: name,
+ DisplayName: displayName,
Description: description,
Icon: icon,
DefaultTTLMillis: defaultTTL.Milliseconds(),
@@ -55,6 +57,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(&defaultTTL, "default-ttl", "", 0, "Edit the template default time before shutdown - workspaces created from this template to this value.")
diff --git a/cli/templateedit_test.go b/cli/templateedit_test.go
index fbfc77d26fdf5..ae39b71b4e00b 100644
--- a/cli/templateedit_test.go
+++ b/cli/templateedit_test.go
@@ -10,8 +10,8 @@ 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"
+ "github.com/coder/coder/testutil"
)
func TestTemplateEdit(t *testing.T) {
@@ -23,14 +23,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.DefaultTTLMillis = ptr.Ref(24 * 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"
defaultTTL := 12 * time.Hour
@@ -39,6 +36,7 @@ func TestTemplateEdit(t *testing.T) {
"edit",
template.Name,
"--name", name,
+ "--display-name", displayName,
"--description", desc,
"--icon", icon,
"--default-ttl", defaultTTL.String(),
@@ -46,7 +44,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)
@@ -54,22 +53,18 @@ 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, defaultTTL.Milliseconds(), updated.DefaultTTLMillis)
})
-
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.DefaultTTLMillis = ptr.Ref(24 * time.Hour.Milliseconds())
- })
+ template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
// Test the cli command.
cmdArgs := []string{
@@ -84,7 +79,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")
@@ -96,4 +92,36 @@ func TestTemplateEdit(t *testing.T) {
assert.Equal(t, template.Icon, updated.Icon)
assert.Equal(t, template.DefaultTTLMillis, updated.DefaultTTLMillis)
})
+ 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)
+
+ ctx, _ := testutil.Context(t)
+ err := cmd.ExecuteContext(ctx)
+
+ require.Error(t, err, "client call must fail")
+ _, isSdkError := codersdk.AsError(err)
+ require.True(t, isSdkError, "sdk error is expected")
+
+ // 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)
+ })
}
diff --git a/coderd/database/databasefake/databasefake.go b/coderd/database/databasefake/databasefake.go
index bb5dcf5377ce4..9d90e182b2a76 100644
--- a/coderd/database/databasefake/databasefake.go
+++ b/coderd/database/databasefake/databasefake.go
@@ -1303,6 +1303,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.DefaultTtl = arg.DefaultTtl
diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql
index dfaf8e0451a4a..ed8ebcee65b91 100644
--- a/coderd/database/dump.sql
+++ b/coderd/database/dump.sql
@@ -353,11 +353,14 @@ 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
);
COMMENT ON COLUMN templates.default_ttl IS 'The default duration for auto-stop for workspaces created from this template.';
+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/migrations/000075_template_display_name.down.sql b/coderd/database/migrations/000075_template_display_name.down.sql
new file mode 100644
index 0000000000000..d12e2af3a5462
--- /dev/null
+++ b/coderd/database/migrations/000075_template_display_name.down.sql
@@ -0,0 +1 @@
+ALTER TABLE templates DROP COLUMN display_name;
diff --git a/coderd/database/migrations/000075_template_display_name.up.sql b/coderd/database/migrations/000075_template_display_name.up.sql
new file mode 100644
index 0000000000000..9c44347cc6789
--- /dev/null
+++ b/coderd/database/migrations/000075_template_display_name.up.sql
@@ -0,0 +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.';
diff --git a/coderd/database/models.go b/coderd/database/models.go
index f457dede07eec..58a849fd25b11 100644
--- a/coderd/database/models.go
+++ b/coderd/database/models.go
@@ -589,6 +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"`
+ // Display name is a custom, human-friendly template name that user can set.
+ 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 fc6cbfa9806eb..5fef1c7149cb8 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, default_ttl, created_by, icon, user_acl, group_acl
+ 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
FROM
templates
WHERE
@@ -3046,13 +3046,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, default_ttl, created_by, icon, user_acl, group_acl
+ 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
FROM
templates
WHERE
@@ -3087,12 +3088,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, default_ttl, created_by, icon, user_acl, group_acl FROM 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 FROM templates
ORDER BY (name, id) ASC
`
@@ -3120,6 +3122,7 @@ func (q *sqlQuerier) GetTemplates(ctx context.Context) ([]Template, error) {
&i.Icon,
&i.UserACL,
&i.GroupACL,
+ &i.DisplayName,
); err != nil {
return nil, err
}
@@ -3136,7 +3139,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
+ 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
FROM
templates
WHERE
@@ -3199,6 +3202,7 @@ func (q *sqlQuerier) GetTemplatesWithFilter(ctx context.Context, arg GetTemplate
&i.Icon,
&i.UserACL,
&i.GroupACL,
+ &i.DisplayName,
); err != nil {
return nil, err
}
@@ -3228,10 +3232,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) RETURNING id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, 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, default_ttl, created_by, icon, user_acl, group_acl, display_name
`
type InsertTemplateParams struct {
@@ -3248,6 +3253,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) {
@@ -3265,6 +3271,7 @@ func (q *sqlQuerier) InsertTemplate(ctx context.Context, arg InsertTemplateParam
arg.Icon,
arg.UserACL,
arg.GroupACL,
+ arg.DisplayName,
)
var i Template
err := row.Scan(
@@ -3282,6 +3289,7 @@ func (q *sqlQuerier) InsertTemplate(ctx context.Context, arg InsertTemplateParam
&i.Icon,
&i.UserACL,
&i.GroupACL,
+ &i.DisplayName,
)
return i, err
}
@@ -3295,7 +3303,7 @@ SET
WHERE
id = $3
RETURNING
- id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl
+ 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
`
type UpdateTemplateACLByIDParams struct {
@@ -3322,6 +3330,7 @@ func (q *sqlQuerier) UpdateTemplateACLByID(ctx context.Context, arg UpdateTempla
&i.Icon,
&i.UserACL,
&i.GroupACL,
+ &i.DisplayName,
)
return i, err
}
@@ -3376,11 +3385,12 @@ SET
description = $3,
default_ttl = $4,
name = $5,
- icon = $6
+ icon = $6,
+ display_name = $7
WHERE
id = $1
RETURNING
- id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl
+ 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
`
type UpdateTemplateMetaByIDParams struct {
@@ -3390,6 +3400,7 @@ type UpdateTemplateMetaByIDParams struct {
DefaultTtl int64 `db:"default_ttl" json:"default_ttl"`
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) {
@@ -3400,6 +3411,7 @@ func (q *sqlQuerier) UpdateTemplateMetaByID(ctx context.Context, arg UpdateTempl
arg.DefaultTtl,
arg.Name,
arg.Icon,
+ arg.DisplayName,
)
var i Template
err := row.Scan(
@@ -3417,6 +3429,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 7063b87075c6f..0757f823d6e40 100644
--- a/coderd/database/queries/templates.sql
+++ b/coderd/database/queries/templates.sql
@@ -69,10 +69,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) RETURNING *;
+ ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING *;
-- name: UpdateTemplateActiveVersionByID :exec
UPDATE
@@ -100,7 +101,8 @@ SET
description = $3,
default_ttl = $4,
name = $5,
- icon = $6
+ icon = $6,
+ display_name = $7
WHERE
id = $1
RETURNING
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..fb55cbb9ff5c7 100644
--- a/coderd/httpapi/httpapi.go
+++ b/coderd/httpapi/httpapi.go
@@ -33,13 +33,14 @@ func init() {
}
return name
})
+
nameValidator := func(fl validator.FieldLevel) bool {
f := fl.Field().Interface()
str, ok := f.(string)
if !ok {
return false
}
- valid := UsernameValid(str)
+ valid := NameValid(str)
return valid == nil
}
for _, tag := range []string{"username", "template_name", "workspace_name"} {
@@ -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/username.go b/coderd/httpapi/name.go
similarity index 60%
rename from coderd/httpapi/username.go
rename to coderd/httpapi/name.go
index 89a9cce92016a..9327eb5556596 100644
--- a/coderd/httpapi/username.go
+++ b/coderd/httpapi/name.go
@@ -11,22 +11,9 @@ import (
var (
UsernameValidRegex = regexp.MustCompile("^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$")
usernameReplace = regexp.MustCompile("[^a-zA-Z0-9-]*")
-)
-// 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
-}
+ templateDisplayName = regexp.MustCompile(`^[^\s](.*[^\s])?$`)
+)
// UsernameFrom returns a best-effort username from the provided string.
//
@@ -35,7 +22,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, "@")
@@ -43,8 +30,39 @@ 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.
+// 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")
+ }
+ 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
+}
+
+// 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) > 64 {
+ return xerrors.New("must be <= 64 characters")
+ }
+ matched := templateDisplayName.MatchString(str)
+ if !matched {
+ return xerrors.New("must be alphanumeric with spaces")
+ }
+ return nil
+}
diff --git a/coderd/httpapi/username_test.go b/coderd/httpapi/name_test.go
similarity index 62%
rename from coderd/httpapi/username_test.go
rename to coderd/httpapi/name_test.go
index 547d5177c4e56..d07cce59f1a16 100644
--- a/coderd/httpapi/username_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 {
@@ -59,7 +59,62 @@ 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)
+ })
+ }
+}
+
+func TestTemplateDisplayNameValid(t *testing.T) {
+ t.Parallel()
+ // Tests whether display names are valid.
+ testCases := []struct {
+ Name string
+ Valid bool
+ }{
+ {"", true},
+ {"1", true},
+ {"12", true},
+ {"1 2", true},
+ {"123 456", true},
+ {"1234 678901234567890", true},
+ {" ", true},
+ {"S", true},
+ {"a1", true},
+ {"a1K2", true},
+ {"!!!!1 ?????", true},
+ {"k\r\rm", true},
+ {"abcdefghijklmnopqrst", true},
+ {"Wow Test", true},
+ {"abcdefghijklmnopqrstu-", true},
+ {"a1b2c3d4e5f6g7h8i9j0k-", true},
+ {"BANANAS_wow", true},
+ {"test--now", true},
+ {"123456789012345678901234567890123", true},
+ {"1234567890123456789012345678901234567890123456789012345678901234", true},
+ {"-a1b2c3d4e5f6g7h8i9j0k", true},
+
+ {" ", false},
+ {"\t", false},
+ {"\r\r", false},
+ {"\t1 ", false},
+ {" a", false},
+ {"\ra ", false},
+ {" 1", false},
+ {"1 ", false},
+ {" aa", false},
+ {"aa\r", false},
+ {" 12", false},
+ {"12 ", false},
+ {"\fa1", false},
+ {"a1\t", false},
+ {"12345678901234567890123456789012345678901234567890123456789012345", false},
+ }
+ for _, testCase := range testCases {
+ testCase := testCase
+ t.Run(testCase.Name, func(t *testing.T) {
+ t.Parallel()
+ valid := httpapi.TemplateDisplayNameValid(testCase.Name)
require.Equal(t, testCase.Valid, valid == nil)
})
}
@@ -92,7 +147,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/templates.go b/coderd/templates.go
index 8becfd31c04df..69520f585f80d 100644
--- a/coderd/templates.go
+++ b/coderd/templates.go
@@ -472,6 +472,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.DefaultTTLMillis == time.Duration(template.DefaultTtl).Milliseconds() {
return nil
@@ -479,6 +480,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.DefaultTTLMillis) * time.Millisecond
@@ -486,6 +488,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
}
@@ -494,6 +499,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,
DefaultTtl: int64(maxTTL),
@@ -738,6 +744,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,
diff --git a/coderd/templates_test.go b/coderd/templates_test.go
index d095cb2d26ad6..a7f4e0ffce6da 100644
--- a/coderd/templates_test.go
+++ b/coderd/templates_test.go
@@ -283,13 +283,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.DefaultTTLMillis = ptr.Ref(24 * 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",
DefaultTTLMillis: 12 * time.Hour.Milliseconds(),
@@ -305,6 +302,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.DefaultTTLMillis, updated.DefaultTTLMillis)
@@ -314,6 +312,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.DefaultTTLMillis, updated.DefaultTTLMillis)
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
diff --git a/codersdk/organizations.go b/codersdk/organizations.go
index 1cb0ca5f975e2..50608e8e01df3 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,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"`
diff --git a/codersdk/templates.go b/codersdk/templates.go
index 05e845240be8f..786984a1cedfc 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"`
@@ -71,7 +72,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"`
DefaultTTLMillis int64 `json:"default_ttl_ms,omitempty"`
diff --git a/enterprise/audit/table.go b/enterprise/audit/table.go
index 72774ced4e142..18649460d0aa3 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,
diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts
index 7c4e3f2f1feea..cb4e186e4d6dc 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
@@ -612,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
@@ -696,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 default_ttl_ms?: number
diff --git a/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx
index 3bbbc085ef43f..4c03bfa49d2af 100644
--- a/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx
+++ b/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx
@@ -69,6 +69,7 @@ export const TemplateSettingsForm: FC = ({
useFormik({
initialValues: {
name: template.name,
+ display_name: template.display_name,
description: template.description,
// on display, convert from ms => hours
default_ttl_ms: template.default_ttl_ms / MS_HOUR_CONVERSION,
diff --git a/site/src/pages/TemplateSettingsPage/TemplateSettingsPage.test.tsx b/site/src/pages/TemplateSettingsPage/TemplateSettingsPage.test.tsx
index 2d5806c6d8d50..9690f7c5ecea9 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: "Test Template",
description: "A description",
icon: "A string",
default_ttl_ms: 1,
diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts
index c8eed5024afa1..f70079a6b1f4c 100644
--- a/site/src/testHelpers/entities.ts
+++ b/site/src/testHelpers/entities.ts
@@ -189,6 +189,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,