Repository: serverless/serverless (JavaScript) — Bug Fix

Issue:
Lambda onError can't use CloudFormation "Ref", requires ARN string
<!--
1. If you have a question and not a bug/feature request please ask it at http://forum.serverless.com
2. Please check if an issue already exists so there are no duplicates
3. Check out and follow our Guidelines: https://github.com/serverless/serverless/blob/master/CONTRIBUTING.md
4. Fill out the whole template so we have a good overview on the issue
5. Do not remove any section of the template. If something is not applicable leave it empty but leave it in the Issue
6. Please follow the template, otherwise we'll have to ask you to update it
-->

# This is a (Bug Report / Feature Proposal)

## Description

For bug reports:
* What went wrong?

I created an SNS topic 'resource' and tried to reference it in the Lambda function's 'onError' 

```
resources:
  Resources:
    DeadLetterQueue:
      Type: AWS::SNS::Topic
      Properties:
        Subscription:
          - Endpoint: chris+dlqtest@example.com
            Protocol: email

functions:
  DlqTest:
    handler: dlqtest.handler
    onError: 
       Ref: DeadLetterQueue  # doesn't work: "onError config must be provided as a string"
    events:
      - s3:
          bucket: cshenton-dlqtest
          event: s3:ObjectCreated:*
```

It complained:

```
onError config must be provided as a string
```

* What did you expect should have happened?

Framework should accept the CloudFormation reference just like it does for other references to the newly-created resource. E.g., I have in the same serverless.yml this ref that works fine:

```
provider:
  name: aws
  runtime: python3.6
  iamRoleStatements:
    - Effect: Allow
      Action:
        - "sns:Publish"
      Resource: 
        Ref: DeadLetterQueue
```

* What was the config you used?

The entirely of my serverless.yml is above, but not ordered as in that file. Is this the "config" you mean?

* What stacktrace or error message from your provider did you see?
```
onError config must be provided as a string
```

For feature proposals:
* What is the use case that should be solved. The more detail you describe this in the easier it is to understand for us.
* If there is additional config how would it look

Similar or dependent issues:
* Code that looks for 'string' only: https://github.com/serverless/serverless/pull/3609/files
* DLQ plugin that might be helpful: http://forum.serverless.com/t/published-a-lambda-dead-letter-queue-plugin/1200?source_topic_id=2476

## Additional Data

* ***Serverless Framework Version you're using***: 1.19.0 (also seen with 1.18.0)
* ***Operating System***: Mac OSX 10.11.6 El Capitan
* ***Stack Trace***: 
```

  Stack Trace --------------------------------------------
 
ServerlessError: onError config must be provided as a string
    at AwsCompileFunctions.compileFunction (/usr/local/lib/node_modules/serverless/lib/plugins/aws/package/compile/functions/index.js:181:15)
    at serverless.service.getAllFunctions.forEach (/usr/local/lib/node_modules/serverless/lib/plugins/aws/package/compile/functions/index.js:325:39)
    at Array.forEach (native)
    at AwsCompileFunctions.compileFunctions (/usr/local/lib/node_modules/serverless/lib/plugins/aws/package/compile/functions/index.js:325:8)
    at BbPromise.reduce (/usr/local/lib/node_modules/serverless/lib/classes/PluginManager.js:236:55)
From previous event:
    at PluginManager.invoke (/usr/local/lib/node_modules/serverless/lib/classes/PluginManager.js:236:22)
    at PluginManager.run (/usr/local/lib/node_modules/serverless/lib/classes/PluginManager.js:255:17)
    at variables.populateService.then (/usr/local/lib/node_modules/serverless/lib/Serverless.js:99:33)
    at runCallback (timers.js:666:20)
    at tryOnImmediate (timers.js:639:5)
    at processImmediate [as _immediateCallback] (timers.js:611:5)
From previous event:
    at Serverless.run (/usr/local/lib/node_modules/serverless/lib/Serverless.js:86:74)
    at serverless.init.then (/usr/local/lib/node_modules/serverless/bin/serverless:39:50)
 
  Get Support --------------------------------------------
     Docs:          docs.serverless.com
     Bugs:          github.com/serverless/serverless/issues
     Forums:        forum.serverless.com
     Chat:          gitter.im/serverless/serverless
 
  Your Environment Information -----------------------------
     OS:                     darwin
     Node Version:           7.7.4
     Serverless Version:     1.19.0
 ```

* ***Provider Error messages***:

```
chris@Haruspex:ocreva$ sls package
Serverless: Packaging service...
Serverless: Excluding development dependencies...
 
  Serverless Error ---------------------------------------
 
  onError config must be provided as a string
 ```


Discussion:

Resolving patch:
diff --git a/docs/providers/aws/guide/functions.md b/docs/providers/aws/guide/functions.md
index d0d3e425160..fc259198db3 100644
--- a/docs/providers/aws/guide/functions.md
+++ b/docs/providers/aws/guide/functions.md
@@ -325,7 +325,7 @@ provider:
 functions:
   hello:
     handler: handler.hello
-    onError: arn:aws:sns:us-east-1:XXXXXX:test
+    onError: arn:aws:sns:us-east-1:XXXXXX:test # Ref and Fn::ImportValue are supported as well
 ```
 
 ### DLQ with SQS
diff --git a/docs/providers/aws/guide/serverless.yml.md b/docs/providers/aws/guide/serverless.yml.md
index 93521b4021d..e2e388d6376 100644
--- a/docs/providers/aws/guide/serverless.yml.md
+++ b/docs/providers/aws/guide/serverless.yml.md
@@ -101,7 +101,7 @@ functions:
     runtime: nodejs6.10 # Runtime for this specific function. Overrides the default which is set on the provider level
     timeout: 10 # Timeout for this specific function.  Overrides the default set above.
     role: arn:aws:iam::XXXXXX:role/role # IAM role which will be used for this function
-    onError: arn:aws:sns:us-east-1:XXXXXX:sns-topic # Optional SNS topic arn which will be used for the DeadLetterConfig
+    onError: arn:aws:sns:us-east-1:XXXXXX:sns-topic # Optional SNS topic arn (Ref and Fn::ImportValue are supported as well) which will be used for the DeadLetterConfig
     awsKmsKeyArn: arn:aws:kms:us-east-1:XXXXXX:key/some-hash # Optional KMS key arn which will be used for encryption (overwrites the one defined on the service level)
     environment: # Function level environment variables
       functionEnvVar: 12345678
diff --git a/lib/plugins/aws/package/compile/functions/index.js b/lib/plugins/aws/package/compile/functions/index.js
index d9c190f6da4..3924ab2b6d4 100644
--- a/lib/plugins/aws/package/compile/functions/index.js
+++ b/lib/plugins/aws/package/compile/functions/index.js
@@ -176,8 +176,15 @@ class AwsCompileFunctions {
           const errorMessage = 'onError config must be a SNS topic arn or SQS queue arn';
           throw new this.serverless.classes.Error(errorMessage);
         }
+      } else if (this.isArnRefOrImportValue(arn)) {
+        newFunction.Properties.DeadLetterConfig = {
+          TargetArn: arn,
+        };
       } else {
-        const errorMessage = 'onError config must be provided as a string';
+        const errorMessage = [
+          'onError config must be provided as an arn string,',
+          ' Ref or Fn::ImportValue',
+        ].join('');
         throw new this.serverless.classes.Error(errorMessage);
       }
     }
@@ -326,6 +333,11 @@ class AwsCompileFunctions {
   }
 
   // helper functions
+  isArnRefOrImportValue(arn) {
+    return typeof arn === 'object' &&
+      _.some(_.keys(arn), (k) => _.includes(['Ref', 'Fn::ImportValue'], k));
+  }
+
   cfLambdaFunctionTemplate() {
     return {
       Type: 'AWS::Lambda::Function',


Test patch:
diff --git a/lib/plugins/aws/package/compile/functions/index.test.js b/lib/plugins/aws/package/compile/functions/index.test.js
index 7dc8c70f3e1..b113af97046 100644
--- a/lib/plugins/aws/package/compile/functions/index.test.js
+++ b/lib/plugins/aws/package/compile/functions/index.test.js
@@ -55,6 +55,16 @@ describe('AwsCompileFunctions', () => {
       expect(awsCompileFunctions.provider).to.be.instanceof(AwsProvider));
   });
 
+  describe('#isArnRefOrImportValue()', () => {
+    it('should accept a Ref', () =>
+      expect(awsCompileFunctions.isArnRefOrImportValue({ Ref: 'DLQ' })).to.equal(true));
+    it('should accept a Fn::ImportValue', () =>
+      expect(awsCompileFunctions.isArnRefOrImportValue({ 'Fn::ImportValue': 'DLQ' }))
+        .to.equal(true));
+    it('should reject other objects', () =>
+      expect(awsCompileFunctions.isArnRefOrImportValue({ Blah: 'vtha' })).to.equal(false));
+  });
+
   describe('#compileFunctions()', () => {
     it('should use service artifact if not individually', () => {
       awsCompileFunctions.serverless.service.package.individually = false;
@@ -664,6 +674,96 @@ describe('AwsCompileFunctions', () => {
           expect(() => { awsCompileFunctions.compileFunctions(); })
             .to.throw(Error, 'only supports SNS');
         });
+
+        it('should create necessary resources if a Ref is provided', () => {
+          awsCompileFunctions.serverless.service.functions = {
+            func: {
+              handler: 'func.function.handler',
+              name: 'new-service-dev-func',
+              onError: {
+                Ref: 'DLQ',
+              },
+            },
+          };
+
+          const compiledFunction = {
+            Type: 'AWS::Lambda::Function',
+            DependsOn: [
+              'FuncLogGroup',
+              'IamRoleLambdaExecution',
+            ],
+            Properties: {
+              Code: {
+                S3Key: `${s3Folder}/${s3FileName}`,
+                S3Bucket: { Ref: 'ServerlessDeploymentBucket' },
+              },
+              FunctionName: 'new-service-dev-func',
+              Handler: 'func.function.handler',
+              MemorySize: 1024,
+              Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] },
+              Runtime: 'nodejs4.3',
+              Timeout: 6,
+              DeadLetterConfig: {
+                TargetArn: {
+                  Ref: 'DLQ',
+                },
+              },
+            },
+          };
+
+          awsCompileFunctions.compileFunctions();
+
+          const compiledCfTemplate = awsCompileFunctions.serverless.service.provider
+            .compiledCloudFormationTemplate;
+
+          const functionResource = compiledCfTemplate.Resources.FuncLambdaFunction;
+          expect(functionResource).to.deep.equal(compiledFunction);
+        });
+
+        it('should create necessary resources if a Fn::ImportValue is provided', () => {
+          awsCompileFunctions.serverless.service.functions = {
+            func: {
+              handler: 'func.function.handler',
+              name: 'new-service-dev-func',
+              onError: {
+                'Fn::ImportValue': 'DLQ',
+              },
+            },
+          };
+
+          const compiledFunction = {
+            Type: 'AWS::Lambda::Function',
+            DependsOn: [
+              'FuncLogGroup',
+              'IamRoleLambdaExecution',
+            ],
+            Properties: {
+              Code: {
+                S3Key: `${s3Folder}/${s3FileName}`,
+                S3Bucket: { Ref: 'ServerlessDeploymentBucket' },
+              },
+              FunctionName: 'new-service-dev-func',
+              Handler: 'func.function.handler',
+              MemorySize: 1024,
+              Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] },
+              Runtime: 'nodejs4.3',
+              Timeout: 6,
+              DeadLetterConfig: {
+                TargetArn: {
+                  'Fn::ImportValue': 'DLQ',
+                },
+              },
+            },
+          };
+
+          awsCompileFunctions.compileFunctions();
+
+          const compiledCfTemplate = awsCompileFunctions.serverless.service.provider
+            .compiledCloudFormationTemplate;
+
+          const functionResource = compiledCfTemplate.Resources.FuncLambdaFunction;
+          expect(functionResource).to.deep.equal(compiledFunction);
+        });
       });
 
       describe('when IamRoleLambdaExecution is not used', () => {