diff --git a/pkg/util/validation/stack.go b/pkg/util/validation/stack.go index 51efa70dd..230d96291 100644 --- a/pkg/util/validation/stack.go +++ b/pkg/util/validation/stack.go @@ -31,17 +31,32 @@ func validateStackName(s string) error { return errors.New("a stack name may only contain alphanumeric, hyphens, underscores, or periods") } +// validateStackTagName checks if s is a valid stack tag name, otherwise returns a descriptive error. +// This should match the stack naming rules enforced by the Pulumi Service. +func validateStackTagName(s string) error { + const maxTagName = 40 + + if len(s) == 0 { + return errors.Errorf("invalid stack tag %q", s) + } + if len(s) > maxTagName { + return errors.Errorf("stack tag %q is too long (max length %d characters)", s, maxTagName) + } + + var tagNameRE = regexp.MustCompile("^[a-zA-Z0-9-_.:]{1,40}$") + if tagNameRE.MatchString(s) { + return nil + } + return errors.New("stack tag names may only contain alphanumerics, hyphens, underscores, periods, or colons") +} + // ValidateStackTags validates the tag names and values. func ValidateStackTags(tags map[apitype.StackTagName]string) error { - const maxTagName = 40 const maxTagValue = 256 for t, v := range tags { - if len(t) == 0 { - return errors.Errorf("invalid stack tag %q", t) - } - if len(t) > maxTagName { - return errors.Errorf("stack tag %q is too long (max length %d characters)", t, maxTagName) + if err := validateStackTagName(t); err != nil { + return err } if len(v) > maxTagValue { return errors.Errorf("stack tag %q value is too long (max length %d characters)", t, maxTagValue) @@ -59,7 +74,7 @@ func ValidateStackProperties(stack string, tags map[apitype.StackTagName]string) return errors.Errorf("stack name too long (max length %d characters)", maxStackName) } if err := validateStackName(stack); err != nil { - return errors.Wrapf(err, "invalid stack name") + return err } // Ensure tag values won't be rejected by the Pulumi Service. We do not validate that their diff --git a/pkg/util/validation/stack_test.go b/pkg/util/validation/stack_test.go new file mode 100644 index 000000000..9a5c5a2b3 --- /dev/null +++ b/pkg/util/validation/stack_test.go @@ -0,0 +1,79 @@ +package validation + +import ( + "fmt" + "strings" + "testing" + + "github.com/pulumi/pulumi/pkg/apitype" + "github.com/stretchr/testify/assert" +) + +func TestValidateStackTag(t *testing.T) { + t.Run("valid tags", func(t *testing.T) { + names := []string{ + "tag-name", + "-", + "..", + "foo:bar:baz", + "__underscores__", + "AaBb123", + } + + for _, name := range names { + t.Run(name, func(t *testing.T) { + tags := map[apitype.StackTagName]string{ + name: "tag-value", + } + + err := ValidateStackTags(tags) + assert.NoError(t, err) + }) + } + }) + + t.Run("invalid stack tag names", func(t *testing.T) { + var names = []string{ + "tag!", + "something with spaces", + "escape\nsequences\there", + "😄", + "foo***bar", + } + + for _, name := range names { + t.Run(name, func(t *testing.T) { + tags := map[apitype.StackTagName]string{ + name: "tag-value", + } + + err := ValidateStackTags(tags) + assert.Error(t, err) + msg := "stack tag names may only contain alphanumerics, hyphens, underscores, periods, or colons" + assert.Equal(t, err.Error(), msg) + }) + } + }) + + t.Run("too long tag name", func(t *testing.T) { + tags := map[apitype.StackTagName]string{ + strings.Repeat("v", 41): "tag-value", + } + + err := ValidateStackTags(tags) + assert.Error(t, err) + msg := fmt.Sprintf("stack tag %q is too long (max length %d characters)", strings.Repeat("v", 41), 40) + assert.Equal(t, err.Error(), msg) + }) + + t.Run("too long tag value", func(t *testing.T) { + tags := map[apitype.StackTagName]string{ + "tag-name": strings.Repeat("v", 257), + } + + err := ValidateStackTags(tags) + assert.Error(t, err) + msg := fmt.Sprintf("stack tag %q value is too long (max length %d characters)", "tag-name", 256) + assert.Equal(t, err.Error(), msg) + }) +}