Repository: serverless/serverless (JavaScript) — Feature

Issue:
Support cloudformation intrinsic function reference for SNS event
# This is a Feature Proposal

## Description
Allow aws sns event definitions to use [cloudformation intrinsic function references](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference.html) to refer to an existing resource in the cloudformation template.

```yaml
functions:
  consumer:
    handler: consumer.handler
    environment:
      SQS_QUEUE_URL:
        Ref: TaskQueue
      WORKER_LAMBDA_FUNCTION_NAME:
        Fn::GetAtt:
          - WorkerLambdaFunction
          - Arn
    events:
      - schedule: rate(1 hour)
      - sns:
          Fn::GetAtt:
            - TaskNotification
            - Arn
        
resources:
  Resources:
    TaskNotification:
      Type: AWS::SNS::Topic
      Properties: 
        TopicName: ${{self:service}}-${{self:provider.stage}}-task-notification
        Subscription:
          SNS Subscription
```

For feature proposals:
Normalized resource names is making it hard to refer to resource created by the serverless template, its really brittle at the moment and can get out of sync very quickly.
Using intrinsic function reference will allow you can generate the name in the resources section and use it to create the sns subscription, without any magic strings.

| AWS Resource          |  Name Template                                          | Example                       |
|---                    |---                                                      | ---                           |
|SNS::Topic | SNSTopic{normalizedTopicName} | SNSTopicSometopic |
|SNS::Subscription | {normalizedFunctionName}SnsSubscription{normalizedTopicName} | HelloSnsSubscriptionSomeTopic |




Discussion:
+1 for this
This is blocking me in a specific context. I have a SNS that I did build in the `resources` section with some specific cloud formation parameters that I want to reuse in the function's events (trigger) but I cannot use any intrinsic cloud formation functions like `Fn::GetAtt`, `Fn::Join` nor any intrinsic function refs like `Ref: AWS::AccountId`.
@pmuens: Could you add yet another label to the issue as this is a blocker for me. Thank you.
@andrei-ionescu thanks for the feedback. I just added the `help-wanted` label...
@pmuens, @fivepapertigers, @gertjvr: Please checkout this fix.

Resolving patch:
diff --git a/docs/providers/aws/events/sns.md b/docs/providers/aws/events/sns.md
index a2b8689b391..3481150fa95 100644
--- a/docs/providers/aws/events/sns.md
+++ b/docs/providers/aws/events/sns.md
@@ -50,6 +50,27 @@ functions:
       - sns: arn:xxx
 ```
 
+Or with intrinsic CloudFormation function like `Fn::Join` or `Fn::GetAtt`.
+
+```yml
+functions:
+  dispatcher:
+    handler: dispatcher.dispatch
+    events:
+      - sns:
+          arn:
+            Fn::Join:
+              - ""
+              - - "arn:aws:sns:"
+                - Ref: "AWS::Region"
+                - ":"
+                - Ref: "AWS::AccountId"
+                - ":MyCustomTopic"
+          topicName: MyCustomTopic
+```
+
+**Note:** It is important to know that `topicArn` must contain the value given in the `topicName` property.
+
 ## Setting a display name
 
 This event definition ensures that the `aggregator` function gets called every time a message is sent to the
diff --git a/lib/plugins/aws/package/compile/events/sns/index.js b/lib/plugins/aws/package/compile/events/sns/index.js
index 32cb99bb279..1fcba97d217 100644
--- a/lib/plugins/aws/package/compile/events/sns/index.js
+++ b/lib/plugins/aws/package/compile/events/sns/index.js
@@ -26,22 +26,55 @@ class AwsCompileSNSEvents {
             let displayName = '';
 
             if (typeof event.sns === 'object') {
-              ['topicName', 'displayName'].forEach((property) => {
-                if (typeof event.sns[property] === 'string') {
-                  return;
+              if (event.sns.arn) {
+                if (typeof event.sns.arn === 'object' ||
+                  typeof event.sns.arn === 'string') {
+                  if (event.sns.topicName && typeof event.sns.topicName === 'string') {
+                    topicArn = event.sns.arn;
+                    topicName = event.sns.topicName;
+                  } else {
+                    const errorMessage = [
+                      'Missing or invalid topicName property for sns event',
+                      ` in function "${functionName}"`,
+                      ' The correct syntax is: sns: topic-name-or-arn',
+                      ' OR an object with ',
+                      ' arn and topicName OR',
+                      ' topicName and displayName.',
+                      ' Please check the docs for more info.',
+                    ].join('');
+                    throw new this.serverless.classes
+                      .Error(errorMessage);
+                  }
+                } else {
+                  const errorMessage = [
+                    'Invalid value type provided .arn property for sns event',
+                    ` in function ${functionName}`,
+                    ' The correct types are: object, string.',
+                    ' Please check the docs for more info.',
+                  ].join('');
+                  throw new this.serverless.classes
+                    .Error(errorMessage);
                 }
-                const errorMessage = [
-                  `Missing or invalid "${property}" property for sns event`,
-                  ` in function ${functionName}`,
-                  ' The correct syntax is: sns: topic-name-or-arn',
-                  ' OR an object with "topicName" AND "displayName" strings.',
-                  ' Please check the docs for more info.',
-                ].join('');
-                throw new this.serverless.classes
-                  .Error(errorMessage);
-              });
-              topicName = event.sns.topicName;
-              displayName = event.sns.displayName;
+              } else {
+                ['topicName', 'displayName'].forEach((property) => {
+                  if (typeof event.sns[property] === 'string') {
+                    return;
+                  }
+                  const errorMessage = [
+                    'Missing or invalid topicName property for sns event',
+                    ` in function "${functionName}"`,
+                    ' The correct syntax is: sns: topic-name-or-arn',
+                    ' OR an object with ',
+                    ' arn and topicName OR',
+                    ' topicName and displayName.',
+                    ' Please check the docs for more info.',
+                  ].join('');
+                  throw new this.serverless.classes
+                    .Error(errorMessage);
+                });
+                displayName = event.sns.displayName;
+                topicName = event.sns.topicName;
+              }
             } else if (typeof event.sns === 'string') {
               if (event.sns.indexOf('arn:') === 0) {
                 topicArn = event.sns;


Test patch:
diff --git a/lib/plugins/aws/package/compile/events/sns/index.test.js b/lib/plugins/aws/package/compile/events/sns/index.test.js
index ec0c50e2e04..d59af889eef 100644
--- a/lib/plugins/aws/package/compile/events/sns/index.test.js
+++ b/lib/plugins/aws/package/compile/events/sns/index.test.js
@@ -157,5 +157,48 @@ describe('AwsCompileSNSEvents', () => {
         .provider.compiledCloudFormationTemplate.Resources.FirstLambdaPermissionFooSNS.Type
       ).to.equal('AWS::Lambda::Permission');
     });
+
+    it('should raise an error when only arn is present', () => {
+      awsCompileSNSEvents.serverless.service.functions = {
+        first: {
+          events: [
+            {
+              sns: {
+                arn: 'arn:aws:sns:region:accountid:bar',
+              },
+            },
+          ],
+        },
+      };
+
+      expect(() => { awsCompileSNSEvents.compileSNSEvents(); }).to.throw(Error);
+    });
+
+    it('should not create SNS topic when arn is provided', () => {
+      awsCompileSNSEvents.serverless.service.functions = {
+        first: {
+          events: [
+            {
+              sns: {
+                topicName: 'bar',
+                arn: 'arn:aws:sns:region:accountid:bar',
+              },
+            },
+          ],
+        },
+      };
+
+      awsCompileSNSEvents.compileSNSEvents();
+
+      expect(Object.keys(awsCompileSNSEvents.serverless.service
+        .provider.compiledCloudFormationTemplate.Resources)
+      ).to.have.length(2);
+      expect(awsCompileSNSEvents.serverless.service
+        .provider.compiledCloudFormationTemplate.Resources.FirstSnsSubscriptionBar.Type
+      ).to.equal('AWS::SNS::Subscription');
+      expect(awsCompileSNSEvents.serverless.service
+        .provider.compiledCloudFormationTemplate.Resources.FirstLambdaPermissionBarSNS.Type
+      ).to.equal('AWS::Lambda::Permission');
+    });
   });
 });