Repository: serverless/serverless (JavaScript)
Date: 2016-07-28 21:23:12+00:00
Task type: Bug Fix

Issue:
Service will not redeploy correctly if new functions are added above old ones
##### Serverless Framework Version:  v1.0.0-alpha.2
##### Operating System:  OSX
##### Expected Behavior:

When I add a new function to the serverless.yaml file. The service is redeployed correctly
##### Actual Behavior:

If the new function is created above an existing functions the deployment fails and is rolled back.

Noticed this when adding a new one as I like to keep the functions in order alphabetically
##### Stack-Trace (Optional):

  Serverless Error ---------------------------------------

```
 An error occurred while provisioning your cloudformation:
 The following resource(s) failed to create: [ResourceApigEvent1].
```

  Stack Trace --------------------------------------------

ServerlessError: An error occurred while provisioning your cloudformation: The following resource(s) failed to create: [ResourceApigEvent1].
    at /Users/jsutherland/Source/serverless/lib/plugins/aws/deploy/lib/updateStack.js:56:33
    at tryCatcher (/Users/jsutherland/Source/serverless/node_modules/bluebird/js/release/util.js:16:23)
    at Promise._settlePromiseFromHandler (/Users/jsutherland/Source/serverless/node_modules/bluebird/js/release/promise.js:504:31)
    at Promise._settlePromise (/Users/jsutherland/Source/serverless/node_modules/bluebird/js/release/promise.js:561:18)
    at Promise._settlePromise0 (/Users/jsutherland/Source/serverless/node_modules/bluebird/js/release/promise.js:606:10)
    at Promise._settlePromises (/Users/jsutherland/Source/serverless/node_modules/bluebird/js/release/promise.js:685:18)
    at Async._drainQueue (/Users/jsutherland/Source/serverless/node_modules/bluebird/js/release/async.js:138:16)
    at Async._drainQueues (/Users/jsutherland/Source/serverless/node_modules/bluebird/js/release/async.js:148:10)
    at Immediate.Async.drainQueues [as _onImmediate](/Users/jsutherland/Source/serverless/node_modules/bluebird/js/release/async.js:17:14)
    at processImmediate [as _immediateCallback](timers.js:383:17)

  Get Support --------------------------------------------
     Docs:          v1.docs.serverless.com
     Bugs:          github.com/serverless/serverless/issues



Issue discussion:
Intersting!!!! Nice catch! So if I understood this correctly, the order of the functions defined in `serverless.yml` matters?

Hmmm yeah I guess that makes sense because we currently map the endpoints/resources according to index.

Yea basically you always need to add to the bottom. Yup, should look at mapping them to the function name in some way so they can be targeted by that rather than index.

After having done a fair bit of investigation, and learning about cloudformation, for #1684 there are a few issues in this space with the way the current updates are happening.

For now the best thing is to remove / deploy when you're making changes as a lot of stuff currently won't get updated.

Exciting stuff though. Really liking the new v1.0 and I think it will be awesome once the bugs are ironed out!


Solution patch:
diff --git a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/methods.js b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/methods.js
index 1c934cfd349..4437face8ce 100644
--- a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/methods.js
+++ b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/methods.js
@@ -31,8 +31,7 @@ module.exports = {
           const resourceLogicalId = this.resourceLogicalIds[path];
           const normalizedMethod = method[0].toUpperCase() +
             method.substr(1).toLowerCase();
-
-          const extractedResourceId = resourceLogicalId.match(/\d+$/)[0];
+          const extractedResourceId = resourceLogicalId.match(/ResourceApigEvent(.*)/)[1];
 
           // universal velocity template
           // provides
diff --git a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/resources.js b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/resources.js
index 83981c9be81..c5aed0fb6f8 100644
--- a/lib/plugins/aws/deploy/compile/events/apiGateway/lib/resources.js
+++ b/lib/plugins/aws/deploy/compile/events/apiGateway/lib/resources.js
@@ -5,6 +5,7 @@ const _ = require('lodash');
 
 module.exports = {
   compileResources() {
+    this.resourceFunctions = [];
     this.resourcePaths = [];
     this.resourceLogicalIds = {};
 
@@ -31,6 +32,7 @@ module.exports = {
           while (path !== '') {
             if (this.resourcePaths.indexOf(path) === -1) {
               this.resourcePaths.push(path);
+              this.resourceFunctions.push(functionName);
             }
 
             const splittedPath = path.split('/');
@@ -41,6 +43,8 @@ module.exports = {
       });
     });
 
+    const capitalizeAlphaNumericPath = (path) => _.capitalize(path.replace(/[^0-9A-Za-z]/g, ''));
+
     // ['users', 'users/create', 'users/create/something']
     this.resourcePaths.forEach(path => {
       const resourcesArray = path.split('/');
@@ -48,7 +52,9 @@ module.exports = {
       const resourceName = path.split('/')[path.split('/').length - 1];
       const resourcePath = path;
       const resourceIndex = this.resourcePaths.indexOf(resourcePath);
-      const resourceLogicalId = `ResourceApigEvent${resourceIndex}`;
+      const resourceFunction = _.capitalize(this.resourceFunctions[resourceIndex]) +
+        resourcesArray.map(capitalizeAlphaNumericPath).join('');
+      const resourceLogicalId = `ResourceApigEvent${resourceFunction}`;
       this.resourceLogicalIds[resourcePath] = resourceLogicalId;
       resourcesArray.pop();
 
@@ -58,7 +64,9 @@ module.exports = {
       } else {
         const resourceParentPath = resourcesArray.join('/');
         const resourceParentIndex = this.resourcePaths.indexOf(resourceParentPath);
-        resourceParentId = `{ "Ref" : "ResourceApigEvent${resourceParentIndex}" }`;
+        const resourceParentFunction = _.capitalize(this.resourceFunctions[resourceParentIndex]) +
+          resourcesArray.map(capitalizeAlphaNumericPath).join('');
+        resourceParentId = `{ "Ref" : "ResourceApigEvent${resourceParentFunction}" }`;
       }
 
       const resourceTemplate = `
@@ -76,6 +84,7 @@ module.exports = {
         [resourceLogicalId]: JSON.parse(resourceTemplate),
       };
 
+
       _.merge(this.serverless.service.resources.Resources,
         resourceObject);
     });


Test patch:
diff --git a/lib/plugins/aws/deploy/compile/events/apiGateway/tests/resources.js b/lib/plugins/aws/deploy/compile/events/apiGateway/tests/resources.js
index 30ad2e2881b..5459bcf3798 100644
--- a/lib/plugins/aws/deploy/compile/events/apiGateway/tests/resources.js
+++ b/lib/plugins/aws/deploy/compile/events/apiGateway/tests/resources.js
@@ -24,6 +24,18 @@ describe('#compileResources()', () => {
           {
             http: 'GET bar/foo',
           },
+          {
+            http: 'GET bar/{id}',
+          },
+          {
+            http: 'GET bar/{id}/foobar',
+          },
+          {
+            http: 'GET bar/{foo_id}',
+          },
+          {
+            http: 'GET bar/{foo_id}/foobar',
+          },
         ],
       },
     };
@@ -31,7 +43,8 @@ describe('#compileResources()', () => {
 
   it('should construct the correct resourcePaths array', () => awsCompileApigEvents
     .compileResources().then(() => {
-      const expectedResourcePaths = ['foo/bar', 'foo', 'bar/foo', 'bar'];
+      const expectedResourcePaths = ['foo/bar', 'foo', 'bar/foo', 'bar', 'bar/{id}',
+        'bar/{id}/foobar', 'bar/{foo_id}', 'bar/{foo_id}/foobar'];
       expect(awsCompileApigEvents.resourcePaths).to.deep.equal(expectedResourcePaths);
     })
   );
@@ -39,10 +52,14 @@ describe('#compileResources()', () => {
   it('should construct the correct resourceLogicalIds object', () => awsCompileApigEvents
     .compileResources().then(() => {
       const expectedResourceLogicalIds = {
-        'foo/bar': 'ResourceApigEvent0',
-        foo: 'ResourceApigEvent1',
-        'bar/foo': 'ResourceApigEvent2',
-        bar: 'ResourceApigEvent3',
+        'foo/bar': 'ResourceApigEventFirstFooBar',
+        foo: 'ResourceApigEventFirstFoo',
+        'bar/{id}/foobar': 'ResourceApigEventFirstBarIdFoobar',
+        'bar/{id}': 'ResourceApigEventFirstBarId',
+        'bar/{foo_id}/foobar': 'ResourceApigEventFirstBarFooidFoobar',
+        'bar/{foo_id}': 'ResourceApigEventFirstBarFooid',
+        'bar/foo': 'ResourceApigEventFirstBarFoo',
+        bar: 'ResourceApigEventFirstBar',
       };
       expect(awsCompileApigEvents.resourceLogicalIds).to.deep.equal(expectedResourceLogicalIds);
     })
@@ -51,13 +68,26 @@ describe('#compileResources()', () => {
   it('should create resource resources when http events are given', () => awsCompileApigEvents
     .compileResources().then(() => {
       expect(awsCompileApigEvents.serverless.service.resources.Resources
-        .ResourceApigEvent0.Properties.PathPart).to.equal('bar');
+        .ResourceApigEventFirstFooBar.Properties.PathPart)
+        .to.equal('bar');
+      expect(awsCompileApigEvents.serverless.service.resources.Resources
+        .ResourceApigEventFirstFooBar.Properties.ParentId.Ref)
+        .to.equal('ResourceApigEventFirstFoo');
+      expect(awsCompileApigEvents.serverless.service.resources.Resources
+        .ResourceApigEventFirstFoo.Properties.ParentId['Fn::GetAtt'][0])
+        .to.equal('RestApiApigEvent');
+      expect(awsCompileApigEvents.serverless.service.resources.Resources
+        .ResourceApigEventFirstBar.Properties.ParentId['Fn::GetAtt'][1])
+        .to.equal('RootResourceId');
       expect(awsCompileApigEvents.serverless.service.resources.Resources
-        .ResourceApigEvent0.Properties.ParentId.Ref).to.equal('ResourceApigEvent1');
+        .ResourceApigEventFirstBarId.Properties.ParentId.Ref)
+        .to.equal('ResourceApigEventFirstBar');
       expect(awsCompileApigEvents.serverless.service.resources.Resources
-        .ResourceApigEvent1.Properties.ParentId['Fn::GetAtt'][0]).to.equal('RestApiApigEvent');
+        .ResourceApigEventFirstBarFooid.Properties.ParentId.Ref)
+        .to.equal('ResourceApigEventFirstBar');
       expect(awsCompileApigEvents.serverless.service.resources.Resources
-        .ResourceApigEvent3.Properties.ParentId['Fn::GetAtt'][1]).to.equal('RootResourceId');
+        .ResourceApigEventFirstBarFooidFoobar.Properties.ParentId.Ref)
+        .to.equal('ResourceApigEventFirstBarFooid');
     })
   );