diff --git a/coderd/httpapi/httpapi.go b/coderd/httpapi/httpapi.go index a832d825ab..bd0555f812 100644 --- a/coderd/httpapi/httpapi.go +++ b/coderd/httpapi/httpapi.go @@ -44,7 +44,7 @@ func init() { valid := NameValid(str) return valid == nil } - for _, tag := range []string{"username", "template_name", "workspace_name", "template_version_name"} { + for _, tag := range []string{"username", "template_name", "workspace_name"} { err := Validate.RegisterValidation(tag, nameValidator) if err != nil { panic(err) @@ -64,6 +64,20 @@ func init() { if err != nil { panic(err) } + + templateVersionNameValidator := func(fl validator.FieldLevel) bool { + f := fl.Field().Interface() + str, ok := f.(string) + if !ok { + return false + } + valid := TemplateVersionNameValid(str) + return valid == nil + } + err = Validate.RegisterValidation("template_version_name", templateVersionNameValidator) + 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 9327eb5556..bea9c17a8b 100644 --- a/coderd/httpapi/name.go +++ b/coderd/httpapi/name.go @@ -12,6 +12,7 @@ var ( UsernameValidRegex = regexp.MustCompile("^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$") usernameReplace = regexp.MustCompile("[^a-zA-Z0-9-]*") + templateVersionName = regexp.MustCompile(`^[a-zA-Z0-9]+(?:[_.-]{1}[a-zA-Z0-9]+)*$`) templateDisplayName = regexp.MustCompile(`^[^\s](.*[^\s])?$`) ) @@ -52,6 +53,18 @@ func NameValid(str string) error { return nil } +// TemplateVersionNameValid returns whether the input string is a valid template version name. +func TemplateVersionNameValid(str string) error { + if len(str) > 64 { + return xerrors.New("must be <= 64 characters") + } + matched := templateVersionName.MatchString(str) + if !matched { + return xerrors.New("must be alphanumeric with underscores and dots") + } + return nil +} + // TemplateDisplayNameValid returns whether the input string is a valid template display name. func TemplateDisplayNameValid(str string) error { if len(str) == 0 { diff --git a/coderd/httpapi/name_test.go b/coderd/httpapi/name_test.go index d07cce59f1..b78a15867c 100644 --- a/coderd/httpapi/name_test.go +++ b/coderd/httpapi/name_test.go @@ -3,6 +3,7 @@ package httpapi_test import ( "testing" + "github.com/moby/moby/pkg/namesgenerator" "github.com/stretchr/testify/require" "github.com/coder/coder/coderd/httpapi" @@ -120,6 +121,57 @@ func TestTemplateDisplayNameValid(t *testing.T) { } } +func TestTemplateVersionNameValid(t *testing.T) { + t.Parallel() + + testCases := []struct { + Name string + Valid bool + }{ + {"1", true}, + {"12", true}, + {"1_2", true}, + {"1-2", true}, + {"cray", true}, + {"123_456", true}, + {"123-456", true}, + {"1234_678901234567890", true}, + {"1234-678901234567890", true}, + {"S", true}, + {"a1", true}, + {"a1K2", true}, + {"fuzzy_bear3", true}, + {"fuzzy-bear3", true}, + {"v1.0.0", true}, + {"heuristic_cray2", true}, + + {"", false}, + {".v1", false}, + {"v1..0", false}, + {"4--4", false}, + {" ", false}, + {"!!!!1 ?????", false}, + } + for _, testCase := range testCases { + testCase := testCase + t.Run(testCase.Name, func(t *testing.T) { + t.Parallel() + valid := httpapi.TemplateVersionNameValid(testCase.Name) + require.Equal(t, testCase.Valid, valid == nil) + }) + } +} + +func TestGeneratedTemplateVersionNameValid(t *testing.T) { + t.Parallel() + + for i := 0; i < 1000; i++ { + name := namesgenerator.GetRandomName(1) + err := httpapi.TemplateVersionNameValid(name) + require.NoError(t, err, "invalid template version name: %s", name) + } +} + func TestFrom(t *testing.T) { t.Parallel() testCases := []struct {