Skip to content

Commit 539d7fa

Browse files
authored
chore: Adds publish transformers and validation (#2585)
Adds publish validator for name/workers. Adds publish converters. Addresses #2550 By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
1 parent 136d3a8 commit 539d7fa

File tree

7 files changed

+269
-7
lines changed

7 files changed

+269
-7
lines changed

internal/pkg/deploy/cloudformation/stack/transformers.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -581,3 +581,38 @@ func convertCommand(command *manifest.CommandOverride) ([]string, error) {
581581
}
582582
return out, nil
583583
}
584+
585+
func convertPublish(p *manifest.PublishConfig) (*template.PublishOpts, error) {
586+
if p == nil || len(p.Topics) == 0 {
587+
return nil, nil
588+
}
589+
publishers := template.PublishOpts{}
590+
// convert the topics to template Topics
591+
for _, topic := range p.Topics {
592+
t, err := convertTopic(topic)
593+
if err != nil {
594+
return nil, err
595+
}
596+
597+
publishers.Topics = append(publishers.Topics, t)
598+
}
599+
600+
return &publishers, nil
601+
}
602+
603+
func convertTopic(t manifest.Topic) (*template.Topics, error) {
604+
err := validatePubSubName(t.Name)
605+
if err != nil {
606+
return nil, err
607+
}
608+
609+
workerErr := validateWorkerNames(t.AllowedWorkers)
610+
if workerErr != nil {
611+
return nil, workerErr
612+
}
613+
614+
return &template.Topics{
615+
Name: t.Name,
616+
AllowedWorkers: t.AllowedWorkers,
617+
}, nil
618+
}

internal/pkg/deploy/cloudformation/stack/transformers_test.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1441,3 +1441,91 @@ func Test_convertImageDependsOn(t *testing.T) {
14411441
})
14421442
}
14431443
}
1444+
1445+
func Test_convertPublish(t *testing.T) {
1446+
testCases := map[string]struct {
1447+
inPublish *manifest.PublishConfig
1448+
1449+
wanted *template.PublishOpts
1450+
wantedError error
1451+
}{
1452+
"empty publish": {
1453+
inPublish: &manifest.PublishConfig{},
1454+
wanted: nil,
1455+
},
1456+
"publish with no topic names": {
1457+
inPublish: &manifest.PublishConfig{
1458+
Topics: []manifest.Topic{
1459+
{},
1460+
},
1461+
},
1462+
wantedError: errMissingPublishTopicField,
1463+
},
1464+
"publish with no workers": {
1465+
inPublish: &manifest.PublishConfig{
1466+
Topics: []manifest.Topic{
1467+
{
1468+
Name: aws.String("topic1"),
1469+
},
1470+
},
1471+
},
1472+
wanted: &template.PublishOpts{
1473+
Topics: []*template.Topics{
1474+
{
1475+
Name: aws.String("topic1"),
1476+
},
1477+
},
1478+
},
1479+
},
1480+
"publish with workers": {
1481+
inPublish: &manifest.PublishConfig{
1482+
Topics: []manifest.Topic{
1483+
{
1484+
Name: aws.String("topic1"),
1485+
AllowedWorkers: []string{"worker1"},
1486+
},
1487+
},
1488+
},
1489+
wanted: &template.PublishOpts{
1490+
Topics: []*template.Topics{
1491+
{
1492+
Name: aws.String("topic1"),
1493+
AllowedWorkers: []string{"worker1"},
1494+
},
1495+
},
1496+
},
1497+
},
1498+
"invalid worker name": {
1499+
inPublish: &manifest.PublishConfig{
1500+
Topics: []manifest.Topic{
1501+
{
1502+
Name: aws.String("topic1"),
1503+
AllowedWorkers: []string{"worker1~~@#$"},
1504+
},
1505+
},
1506+
},
1507+
wantedError: fmt.Errorf("worker name `worker1~~@#$` is invalid: %s", errNameBadFormat),
1508+
},
1509+
"invalid topic name": {
1510+
inPublish: &manifest.PublishConfig{
1511+
Topics: []manifest.Topic{
1512+
{
1513+
Name: aws.String("topic1~~@#$"),
1514+
AllowedWorkers: []string{"worker1"},
1515+
},
1516+
},
1517+
},
1518+
wantedError: errInvalidPubSubTopicName,
1519+
},
1520+
}
1521+
for name, tc := range testCases {
1522+
t.Run(name, func(t *testing.T) {
1523+
got, err := convertPublish(tc.inPublish)
1524+
if tc.wantedError != nil {
1525+
require.EqualError(t, err, tc.wantedError.Error())
1526+
} else {
1527+
require.Equal(t, got, tc.wanted)
1528+
}
1529+
})
1530+
}
1531+
}

internal/pkg/deploy/cloudformation/stack/validate.go

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package stack
66
import (
77
"errors"
88
"fmt"
9+
"regexp"
910

1011
"github.com/aws/aws-sdk-go/aws"
1112
"github.com/aws/copilot-cli/internal/pkg/manifest"
@@ -21,10 +22,11 @@ const (
2122

2223
// Empty field errors.
2324
var (
24-
errNoFSID = errors.New("volume field `efs.id` cannot be empty")
25-
errNoContainerPath = errors.New("`path` cannot be empty")
26-
errNoSourceVolume = errors.New("`source_volume` cannot be empty")
27-
errEmptyEFSConfig = errors.New("bad EFS configuration: `efs` cannot be empty")
25+
errNoFSID = errors.New("volume field `efs.id` cannot be empty")
26+
errNoContainerPath = errors.New("`path` cannot be empty")
27+
errNoSourceVolume = errors.New("`source_volume` cannot be empty")
28+
errEmptyEFSConfig = errors.New("bad EFS configuration: `efs` cannot be empty")
29+
errMissingPublishTopicField = errors.New("topic `name` cannot be empty")
2830
)
2931

3032
// Conditional errors.
@@ -40,6 +42,10 @@ var (
4042
errInvalidSidecarDependsOnStatus = fmt.Errorf("sidecar container dependency status must be one of < %s | %s | %s >", dependsOnStart, dependsOnComplete, dependsOnSuccess)
4143
errEssentialContainerStatus = fmt.Errorf("essential container dependencies can only have status < %s | %s >", dependsOnStart, dependsOnHealthy)
4244
errEssentialSidecarStatus = fmt.Errorf("essential sidecar container dependencies can only have status < %s >", dependsOnStart)
45+
errInvalidPubSubTopicName = errors.New("topic names can only contain letters, numbers, underscores, and hypthens")
46+
errInvalidName = errors.New("names cannot be empty")
47+
errNameTooLong = errors.New("names must not exceed 255 characters")
48+
errNameBadFormat = errors.New("names must start with a letter, contain only lower-case letters, numbers, and hyphens, and have no consecutive or trailing hyphen")
4349
)
4450

4551
// Container dependency status options
@@ -49,6 +55,14 @@ var (
4955
sidecarDependsOnValidStatuses = []string{dependsOnStart, dependsOnComplete, dependsOnSuccess}
5056
)
5157

58+
// Regex options
59+
var (
60+
awsSNSTopicRegexp = regexp.MustCompile(`^[a-zA-Z0-9_-]*$`) // Validates that an expression contains only letters, numbers, underscores, and hyphens.
61+
awsNameRegexp = regexp.MustCompile(`^[a-z][a-z0-9\-]+$`) // Validates that an expression starts with a letter and only contains letters, numbers, and hyphens.
62+
punctuationRegExp = regexp.MustCompile(`[\.\-]{2,}`) // Check for consecutive periods or dashes.
63+
trailingPunctRegExp = regexp.MustCompile(`[\-\.]$`) // Check for trailing dash or dot.
64+
)
65+
5266
// Validate that paths contain only an approved set of characters to guard against command injection.
5367
// We can accept 0-9A-Za-z-_.
5468
func validatePath(input string, maxLength int) error {
@@ -392,3 +406,57 @@ func validateRootDirPath(input string) error {
392406
func validateContainerPath(input string) error {
393407
return validatePath(input, maxDockerContainerPathLength)
394408
}
409+
410+
// ValidatePubSubName validates naming is correct for topics in publishing/subscribing cases, such as naming for a
411+
// SNS Topic intended for a publisher.
412+
func validatePubSubName(name *string) error {
413+
if name == nil || len(aws.StringValue(name)) == 0 {
414+
return errMissingPublishTopicField
415+
}
416+
417+
// Name must contain letters, numbers, and can't use special characters besides underscores and hyphens.
418+
if !awsSNSTopicRegexp.MatchString(aws.StringValue(name)) {
419+
return errInvalidPubSubTopicName
420+
}
421+
422+
return nil
423+
}
424+
425+
func validateWorkerNames(names []string) error {
426+
for _, name := range names {
427+
err := validateSvcName(name)
428+
if err != nil {
429+
return fmt.Errorf("worker name `%s` is invalid: %w", name, err)
430+
}
431+
}
432+
return nil
433+
}
434+
435+
func validateSvcName(name string) error {
436+
if name == "" {
437+
return errInvalidName
438+
}
439+
if len(name) > 255 {
440+
return errNameTooLong
441+
}
442+
if !isCorrectSvcNameFormat(name) {
443+
return errNameBadFormat
444+
}
445+
446+
return nil
447+
}
448+
449+
func isCorrectSvcNameFormat(s string) bool {
450+
if !awsNameRegexp.MatchString(s) {
451+
return false
452+
}
453+
454+
// Check for bad punctuation (no consecutive dashes or dots)
455+
formatMatch := punctuationRegExp.FindStringSubmatch(s)
456+
if len(formatMatch) != 0 {
457+
return false
458+
}
459+
460+
trailingMatch := trailingPunctRegExp.FindStringSubmatch(s)
461+
return len(trailingMatch) == 0
462+
}

internal/pkg/deploy/cloudformation/stack/validate_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,3 +498,70 @@ func TestValidateImageDependsOn(t *testing.T) {
498498
})
499499
}
500500
}
501+
502+
func Test_validatePubSubTopicName(t *testing.T) {
503+
testCases := map[string]struct {
504+
inName *string
505+
506+
wantErr error
507+
}{
508+
"valid topic name": {
509+
inName: aws.String("a-Perfectly_V4l1dString"),
510+
},
511+
"error when no topic name": {
512+
inName: nil,
513+
wantErr: errMissingPublishTopicField,
514+
},
515+
"error when invalid topic name": {
516+
inName: aws.String("OHNO~/`...,"),
517+
wantErr: errInvalidPubSubTopicName,
518+
},
519+
}
520+
for name, tc := range testCases {
521+
t.Run(name, func(t *testing.T) {
522+
err := validatePubSubName(tc.inName)
523+
if tc.wantErr == nil {
524+
require.NoError(t, err)
525+
} else {
526+
require.EqualError(t, err, tc.wantErr.Error())
527+
}
528+
})
529+
}
530+
}
531+
532+
func TestValidateWorkerName(t *testing.T) {
533+
testCases := map[string]struct {
534+
inName []string
535+
536+
wantErr error
537+
}{
538+
"good case": {
539+
inName: []string{"good-name"},
540+
wantErr: nil,
541+
},
542+
"empty name": {
543+
inName: []string{""},
544+
wantErr: fmt.Errorf("worker name `` is invalid: %s", errInvalidName),
545+
},
546+
"contains spaces": {
547+
inName: []string{"a re@!!y b#d n&me"},
548+
wantErr: fmt.Errorf("worker name `a re@!!y b#d n&me` is invalid: %s", errNameBadFormat),
549+
},
550+
"too long": {
551+
inName: []string{"this-is-the-name-that-goes-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-until-it-is-too-long"},
552+
wantErr: fmt.Errorf("worker name `this-is-the-name-that-goes-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-and-on-until-it-is-too-long` is invalid: %s", errNameTooLong),
553+
},
554+
}
555+
556+
for name, tc := range testCases {
557+
t.Run(name, func(t *testing.T) {
558+
err := validateWorkerNames(tc.inName)
559+
560+
if tc.wantErr == nil {
561+
require.NoError(t, err)
562+
} else {
563+
require.EqualError(t, err, tc.wantErr.Error())
564+
}
565+
})
566+
}
567+
}

internal/pkg/template/template_functions.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,11 @@ func generatePublishJSON(topics []*Topics) string {
142142
if aws.StringValue(pb.Name) == "" {
143143
continue
144144
}
145-
publisherMap[aws.StringValue(pb.Name)] = aws.StringValueSlice(pb.AllowedWorkers)
145+
if pb.AllowedWorkers == nil {
146+
publisherMap[aws.StringValue(pb.Name)] = []string{}
147+
} else {
148+
publisherMap[aws.StringValue(pb.Name)] = pb.AllowedWorkers
149+
}
146150
}
147151

148152
out, ok := getJSONMap(publisherMap)

internal/pkg/template/template_functions_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ func TestGeneratePublishJSON(t *testing.T) {
244244
[]*Topics{
245245
{
246246
Name: aws.String("tests"),
247-
AllowedWorkers: aws.StringSlice([]string{"testsWorker1", "testsWorker2"}),
247+
AllowedWorkers: []string{"testsWorker1", "testsWorker2"},
248248
},
249249
},
250250
), "JSON should render correctly")

internal/pkg/template/workload.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ type PublishOpts struct {
212212
// Topics holds information needed to render a SNSTopic in a container definition.
213213
type Topics struct {
214214
Name *string
215-
AllowedWorkers []*string
215+
AllowedWorkers []string
216216
}
217217

218218
// NetworkOpts holds AWS networking configuration for the workloads.

0 commit comments

Comments
 (0)