Repository: serverless/serverless (JavaScript)
Date: 2016-12-09 14:27:59+00:00
Task type: Bug Fix

Issue:
Serverless doesn't properly wait for deployment to finish in case there is a nested stack
# This is a Bug Report

## Description

* What went wrong?
I used resources part of serverless.yml to include a nested stack from a cloudformation file located on s3. I also set the usual serverless resources to be dependent on this stack... Something like this:

~~~~
resources:
  Resources:
    outsideStack:
      Type: AWS::CloudFormation::Stack
      Properties:
        TemplateURL: 'https://xxx.s3.amazonaws.com/user-pool-client.cform'
    IamRoleLambdaExecution:
      DependsOn:
        - outsideStack
~~~~
Everything deploys fine, the nested stack deploys first, and the default iam role and the functions only after that. However the problem is that serverless command returns immediately after the *nested stack* is completed, and doesn't wait for the whole stack to finish. 
One can see this in two ways:
-By observing that even the serverless command is finished, the deployment is still going on in aws CloudFormation console.
-If the stack is created for the first time, the `functions` and `endpoints` report is empty, even there are functions with API Gateway endpoints defined in serverless.yml

* What did you expect should have happened?
For serverless command to wait for the whole stack to finish, and only then return ids of all created endpoints and functions.

* What was the config you used?
AWS

* What stacktrace or error message from your provider did you see?
No stacktrace, everything seemed fine, just empty result:


## Additional Data

* ***Serverless Framework Version you're using***: 1.3.0
* ***Operating System***:MacOS Sierra 
* ***Stack Trace***:
* ***Provider Error messages***:



Issue discussion:
It seems changing line 51 of monitorStack.js from:

~~~~
if (event.ResourceType === 'AWS::CloudFormation::Stack' ) {
~~~~
to:
~~~~
if (event.ResourceType === 'AWS::CloudFormation::Stack' 
    && event.StackId === event.PhysicalResourceId) {
~~~~

fixes it as only in the case of the nested stack those are different (StackId being the whole stack, and PhysicalResourceId being the nested stack)



@tgjorgoski sounds good! Could you provide a PR?

That would be awesome!
@pmuens , sure, though I've never done PR to github, so would probably need some help.
I created branch locally, however when I try to push it, I get this:
~~~~
> git push origin fix-deployment-with-nested-stack
ERROR: Permission to serverless/serverless.git denied to tgjorgoski.
fatal: Could not read from remote repository.
~~~~ 
Hey @tgjorgoski sure thing. 👍
At first you need to fork Serverless and then create a branch in your fork. After that you submit a pull request from your fork to the official Serverless repository.

Here's a guide on how to do that:
https://gist.github.com/Chaser324/ce0505fbed06b947d962

Let us know if you need any further help! Together we make it 💪 

Solution patch:
diff --git a/lib/plugins/aws/lib/monitorStack.js b/lib/plugins/aws/lib/monitorStack.js
index 2e636e3c14a..a85813ffc5a 100644
--- a/lib/plugins/aws/lib/monitorStack.js
+++ b/lib/plugins/aws/lib/monitorStack.js
@@ -48,7 +48,8 @@ module.exports = {
                   let eventStatus = event.ResourceStatus || null;
                   if (eventInRange && eventNotLogged) {
                     // Keep track of stack status
-                    if (event.ResourceType === 'AWS::CloudFormation::Stack') {
+                    if (event.ResourceType === 'AWS::CloudFormation::Stack'
+                      && event.StackName === event.LogicalResourceId) {
                       stackStatus = eventStatus;
                     }
                     // Keep track of first failed event


Test patch:
diff --git a/lib/plugins/aws/lib/monitorStack.test.js b/lib/plugins/aws/lib/monitorStack.test.js
index 4e1ce49cd67..3f18b0086fb 100644
--- a/lib/plugins/aws/lib/monitorStack.test.js
+++ b/lib/plugins/aws/lib/monitorStack.test.js
@@ -56,7 +56,8 @@ describe('monitorStack', () => {
         StackEvents: [
           {
             EventId: '1a2b3c4d',
-            LogicalResourceId: 'mocha',
+            StackName: 'new-service-dev',
+            LogicalResourceId: 'new-service-dev',
             ResourceType: 'AWS::CloudFormation::Stack',
             Timestamp: new Date(),
             ResourceStatus: 'CREATE_IN_PROGRESS',
@@ -67,7 +68,8 @@ describe('monitorStack', () => {
         StackEvents: [
           {
             EventId: '1e2f3g4h',
-            LogicalResourceId: 'mocha',
+            StackName: 'new-service-dev',
+            LogicalResourceId: 'new-service-dev',
             ResourceType: 'AWS::CloudFormation::Stack',
             Timestamp: new Date(),
             ResourceStatus: 'CREATE_COMPLETE',
@@ -103,7 +105,8 @@ describe('monitorStack', () => {
         StackEvents: [
           {
             EventId: '1a2b3c4d',
-            LogicalResourceId: 'mocha',
+            StackName: 'new-service-dev',
+            LogicalResourceId: 'new-service-dev',
             ResourceType: 'AWS::CloudFormation::Stack',
             Timestamp: new Date(),
             ResourceStatus: 'UPDATE_IN_PROGRESS',
@@ -114,7 +117,8 @@ describe('monitorStack', () => {
         StackEvents: [
           {
             EventId: '1e2f3g4h',
-            LogicalResourceId: 'mocha',
+            StackName: 'new-service-dev',
+            LogicalResourceId: 'new-service-dev',
             ResourceType: 'AWS::CloudFormation::Stack',
             Timestamp: new Date(),
             ResourceStatus: 'UPDATE_COMPLETE',
@@ -150,7 +154,8 @@ describe('monitorStack', () => {
         StackEvents: [
           {
             EventId: '1a2b3c4d',
-            LogicalResourceId: 'mocha',
+            StackName: 'new-service-dev',
+            LogicalResourceId: 'new-service-dev',
             ResourceType: 'AWS::CloudFormation::Stack',
             Timestamp: new Date(),
             ResourceStatus: 'DELETE_IN_PROGRESS',
@@ -161,7 +166,8 @@ describe('monitorStack', () => {
         StackEvents: [
           {
             EventId: '1e2f3g4h',
-            LogicalResourceId: 'mocha',
+            StackName: 'new-service-dev',
+            LogicalResourceId: 'new-service-dev',
             ResourceType: 'AWS::CloudFormation::Stack',
             Timestamp: new Date(),
             ResourceStatus: 'DELETE_COMPLETE',
@@ -188,6 +194,192 @@ describe('monitorStack', () => {
       });
     });
 
+    it('should not stop monitoring on CREATE_COMPLETE nested stack status', () => {
+      const describeStackEventsStub = sinon.stub(awsPlugin.provider, 'request');
+      const cfDataMock = {
+        StackId: 'new-service-dev',
+      };
+      const updateStartEvent = {
+        StackEvents: [
+          {
+            EventId: '1a2b3c4d',
+            StackName: 'new-service-dev',
+            LogicalResourceId: 'new-service-dev',
+            ResourceType: 'AWS::CloudFormation::Stack',
+            Timestamp: new Date(),
+            ResourceStatus: 'CREATE_IN_PROGRESS',
+          },
+        ],
+      };
+      const nestedStackEvent = {
+        StackEvents: [
+          {
+            EventId: '1e2f3g4z',
+            StackName: 'new-service-dev',
+            LogicalResourceId: 'nested-stack-name',
+            ResourceType: 'AWS::CloudFormation::Stack',
+            Timestamp: new Date(),
+            ResourceStatus: 'CREATE_COMPLETE',
+          },
+        ],
+      };
+      const updateFinishedEvent = {
+        StackEvents: [
+          {
+            EventId: '1e2f3g4h',
+            StackName: 'new-service-dev',
+            LogicalResourceId: 'new-service-dev',
+            ResourceType: 'AWS::CloudFormation::Stack',
+            Timestamp: new Date(),
+            ResourceStatus: 'CREATE_COMPLETE',
+          },
+        ],
+      };
+
+      describeStackEventsStub.onCall(0).returns(BbPromise.resolve(updateStartEvent));
+      describeStackEventsStub.onCall(1).returns(BbPromise.resolve(nestedStackEvent));
+      describeStackEventsStub.onCall(2).returns(BbPromise.resolve(updateFinishedEvent));
+
+      return awsPlugin.monitorStack('create', cfDataMock, 10).then((stackStatus) => {
+        expect(describeStackEventsStub.callCount).to.be.equal(3);
+        expect(describeStackEventsStub.calledWithExactly(
+          'CloudFormation',
+          'describeStackEvents',
+          {
+            StackName: cfDataMock.StackId,
+          },
+          awsPlugin.options.stage,
+          awsPlugin.options.region
+        )).to.be.equal(true);
+        expect(stackStatus).to.be.equal('CREATE_COMPLETE');
+        awsPlugin.provider.request.restore();
+      });
+    });
+
+    it('should not stop monitoring on UPDATE_COMPLETE nested stack status', () => {
+      const describeStackEventsStub = sinon.stub(awsPlugin.provider, 'request');
+      const cfDataMock = {
+        StackId: 'new-service-dev',
+      };
+      const updateStartEvent = {
+        StackEvents: [
+          {
+            EventId: '1a2b3c4d',
+            StackName: 'new-service-dev',
+            LogicalResourceId: 'new-service-dev',
+            ResourceType: 'AWS::CloudFormation::Stack',
+            Timestamp: new Date(),
+            ResourceStatus: 'UPDATE_IN_PROGRESS',
+          },
+        ],
+      };
+      const nestedStackEvent = {
+        StackEvents: [
+          {
+            EventId: '1e2f3g4z',
+            StackName: 'new-service-dev',
+            LogicalResourceId: 'nested-stack-name',
+            ResourceType: 'AWS::CloudFormation::Stack',
+            Timestamp: new Date(),
+            ResourceStatus: 'UPDATE_COMPLETE',
+          },
+        ],
+      };
+      const updateFinishedEvent = {
+        StackEvents: [
+          {
+            EventId: '1e2f3g4h',
+            StackName: 'new-service-dev',
+            LogicalResourceId: 'new-service-dev',
+            ResourceType: 'AWS::CloudFormation::Stack',
+            Timestamp: new Date(),
+            ResourceStatus: 'UPDATE_COMPLETE',
+          },
+        ],
+      };
+
+      describeStackEventsStub.onCall(0).returns(BbPromise.resolve(updateStartEvent));
+      describeStackEventsStub.onCall(1).returns(BbPromise.resolve(nestedStackEvent));
+      describeStackEventsStub.onCall(2).returns(BbPromise.resolve(updateFinishedEvent));
+
+      return awsPlugin.monitorStack('update', cfDataMock, 10).then((stackStatus) => {
+        expect(describeStackEventsStub.callCount).to.be.equal(3);
+        expect(describeStackEventsStub.calledWithExactly(
+          'CloudFormation',
+          'describeStackEvents',
+          {
+            StackName: cfDataMock.StackId,
+          },
+          awsPlugin.options.stage,
+          awsPlugin.options.region
+        )).to.be.equal(true);
+        expect(stackStatus).to.be.equal('UPDATE_COMPLETE');
+        awsPlugin.provider.request.restore();
+      });
+    });
+
+    it('should not stop monitoring on DELETE_COMPLETE nested stack status', () => {
+      const describeStackEventsStub = sinon.stub(awsPlugin.provider, 'request');
+      const cfDataMock = {
+        StackId: 'new-service-dev',
+      };
+      const updateStartEvent = {
+        StackEvents: [
+          {
+            EventId: '1a2b3c4d',
+            StackName: 'new-service-dev',
+            LogicalResourceId: 'new-service-dev',
+            ResourceType: 'AWS::CloudFormation::Stack',
+            Timestamp: new Date(),
+            ResourceStatus: 'DELETE_IN_PROGRESS',
+          },
+        ],
+      };
+      const nestedStackEvent = {
+        StackEvents: [
+          {
+            EventId: '1e2f3g4z',
+            StackName: 'new-service-dev',
+            LogicalResourceId: 'nested-stack-name',
+            ResourceType: 'AWS::CloudFormation::Stack',
+            Timestamp: new Date(),
+            ResourceStatus: 'DELETE_COMPLETE',
+          },
+        ],
+      };
+      const updateFinishedEvent = {
+        StackEvents: [
+          {
+            EventId: '1e2f3g4h',
+            StackName: 'new-service-dev',
+            LogicalResourceId: 'new-service-dev',
+            ResourceType: 'AWS::CloudFormation::Stack',
+            Timestamp: new Date(),
+            ResourceStatus: 'DELETE_COMPLETE',
+          },
+        ],
+      };
+
+      describeStackEventsStub.onCall(0).returns(BbPromise.resolve(updateStartEvent));
+      describeStackEventsStub.onCall(1).returns(BbPromise.resolve(nestedStackEvent));
+      describeStackEventsStub.onCall(2).returns(BbPromise.resolve(updateFinishedEvent));
+
+      return awsPlugin.monitorStack('removal', cfDataMock, 10).then((stackStatus) => {
+        expect(describeStackEventsStub.callCount).to.be.equal(3);
+        expect(describeStackEventsStub.calledWithExactly(
+          'CloudFormation',
+          'describeStackEvents',
+          {
+            StackName: cfDataMock.StackId,
+          },
+          awsPlugin.options.stage,
+          awsPlugin.options.region
+        )).to.be.equal(true);
+        expect(stackStatus).to.be.equal('DELETE_COMPLETE');
+        awsPlugin.provider.request.restore();
+      });
+    });
+
     it('should keep monitoring until DELETE_COMPLETE or stack not found catch', () => {
       const describeStackEventsStub = sinon.stub(awsPlugin.provider, 'request');
       const cfDataMock = {
@@ -197,7 +389,8 @@ describe('monitorStack', () => {
         StackEvents: [
           {
             EventId: '1a2b3c4d',
-            LogicalResourceId: 'mocha',
+            StackName: 'new-service-dev',
+            LogicalResourceId: 'new-service-dev',
             ResourceType: 'AWS::CloudFormation::Stack',
             Timestamp: new Date(),
             ResourceStatus: 'DELETE_IN_PROGRESS',
@@ -237,7 +430,8 @@ describe('monitorStack', () => {
         StackEvents: [
           {
             EventId: '1a2b3c4d',
-            LogicalResourceId: 'mocha',
+            StackName: 'new-service-dev',
+            LogicalResourceId: 'new-service-dev',
             ResourceType: 'AWS::CloudFormation::Stack',
             Timestamp: new Date(),
             ResourceStatus: 'UPDATE_IN_PROGRESS',
@@ -248,6 +442,7 @@ describe('monitorStack', () => {
         StackEvents: [
           {
             EventId: '1e2f3g4h',
+            StackName: 'new-service-dev',
             LogicalResourceId: 'mochaS3',
             ResourceType: 'S3::Bucket',
             Timestamp: new Date(),
@@ -260,7 +455,8 @@ describe('monitorStack', () => {
         StackEvents: [
           {
             EventId: '1i2j3k4l',
-            LogicalResourceId: 'mocha',
+            StackName: 'new-service-dev',
+            LogicalResourceId: 'new-service-dev',
             ResourceType: 'AWS::CloudFormation::Stack',
             Timestamp: new Date(),
             ResourceStatus: 'UPDATE_ROLLBACK_IN_PROGRESS',
@@ -271,7 +467,8 @@ describe('monitorStack', () => {
         StackEvents: [
           {
             EventId: '1m2n3o4p',
-            LogicalResourceId: 'mocha',
+            StackName: 'new-service-dev',
+            LogicalResourceId: 'new-service-dev',
             ResourceType: 'AWS::CloudFormation::Stack',
             Timestamp: new Date(),
             ResourceStatus: 'ROLLBACK_COMPLETE',
@@ -311,6 +508,7 @@ describe('monitorStack', () => {
         StackEvents: [
           {
             EventId: '1a2b3c4d',
+            StackName: 'new-service-dev',
             LogicalResourceId: 'somebucket',
             ResourceType: 'AWS::S3::Bucket',
             Timestamp: new Date(),
@@ -321,7 +519,8 @@ describe('monitorStack', () => {
         StackEvents: [
           {
             EventId: '1a2b3c4d',
-            LogicalResourceId: 'mocha',
+            StackName: 'new-service-dev',
+            LogicalResourceId: 'new-service-dev',
             ResourceType: 'AWS::CloudFormation::Stack',
             Timestamp: new Date(),
             ResourceStatus: 'UPDATE_IN_PROGRESS',
@@ -332,7 +531,8 @@ describe('monitorStack', () => {
         StackEvents: [
           {
             EventId: '1m2n3o4p',
-            LogicalResourceId: 'mocha',
+            StackName: 'new-service-dev',
+            LogicalResourceId: 'new-service-dev',
             ResourceType: 'AWS::CloudFormation::Stack',
             Timestamp: new Date(),
             ResourceStatus: 'UPDATE_COMPLETE',