Repository: serverless/serverless (JavaScript) — Bug Fix

Issue:
Confusing error messages when creating stages with special characters
# This is a Bug Report

## Description

* What went wrong?

Confusing error messages when creating stages

    sls deploy --stage local-bot
    ...
    Serverless Error ---------------------------------------

      An error occurred: ApiGatewayDeployment1518286877103 - Stage name only allows a-zA-Z0-9_.

    sls deploy --stage local_bot

    Serverless Error ---------------------------------------

      The stack service name "myservice-local_bot" is not valid. A service name should only contain 
      alphanumeric (case sensitive) and hyphens. It should start with an alphabetic character and 
      shouldn't exceed 128 characters.

* What did you expect should have happened?

Let me use `a-zA-Z0-9_.` for my stage name

* What was the config you used?

aws-nodejs (template)

## Additional Data

Your Environment Information -----------------------------
     OS:                     darwin
     Node Version:           9.4.0
     Serverless Version:     1.26.0



Discussion:
Hi @benswinburne , thanks for bringing this up. It is indeed confusing.

The correct solution here would be to disallow hyphens `-` and underscores `_` from Serverless stage names or transform the underscore in the CF stack name to a hyphen, because

(1) CF stack names cannot contain underscores
(2) APIG stage names cannot contain hyphens

So using either of the characters will lead to an error on one of the AWS services/resources.

The transformation solution has to be analyzed, because by using transformations of the name, it could lead to name conflicts in CF.

Resolving patch:
diff --git a/lib/plugins/aws/provider/awsProvider.js b/lib/plugins/aws/provider/awsProvider.js
index 149828fc74b..1b4cc8655e1 100644
--- a/lib/plugins/aws/provider/awsProvider.js
+++ b/lib/plugins/aws/provider/awsProvider.js
@@ -19,6 +19,8 @@ const constants = {
   providerName: 'aws',
 };
 
+const validAPIGatewayStageNamePattern = /^[a-zA-Z0-9_]+$/;
+
 PromiseQueue.configure(BbPromise.Promise);
 
 const impl = {
@@ -199,6 +201,19 @@ class AwsProvider {
         }
       }
     }
+
+    const stage = this.getStage();
+    this.serverless.service.getAllFunctions().forEach(funcName => {
+      _.forEach(this.serverless.service.getAllEventsInFunction(funcName), event => {
+        if (_.has(event, 'http') && !validAPIGatewayStageNamePattern.test(stage)) {
+          throw new Error([
+            `Invalid stage name ${stage}: `,
+            'it should contains only [a-zA-Z0-9] for AWS provider if http event are present ',
+            'since API Gateway stage name cannot contains hyphens.',
+          ].join(''));
+        }
+      });
+    });
   }
 
   /**


Test patch:
diff --git a/lib/plugins/aws/provider/awsProvider.test.js b/lib/plugins/aws/provider/awsProvider.test.js
index 8fe4e5958ff..c2952fe88e4 100644
--- a/lib/plugins/aws/provider/awsProvider.test.js
+++ b/lib/plugins/aws/provider/awsProvider.test.js
@@ -90,6 +90,54 @@ describe('AwsProvider', () => {
       delete process.env.AWS_CLIENT_TIMEOUT;
     });
 
+    describe('validation on construction', () => {
+      it('should not throw an error if stage name contains only alphanumeric', () => {
+        const config = {
+          stage: 'configStage',
+        };
+        serverless = new Serverless(config);
+        expect(() => new AwsProvider(serverless, config)).to.not.throw(Error);
+      });
+
+      it('should not throw an error if stage contains hyphen but http events are absent', () => {
+        const config = {
+          stage: 'config-stage',
+        };
+        serverless = new Serverless(config);
+        expect(() => new AwsProvider(serverless, config)).to.not.throw(Error);
+      });
+
+      it('should throw an error if stage contains hyphen and http events are present', () => {
+        const config = {
+          stage: 'config-stage',
+        };
+        serverless = new Serverless(config);
+
+        const serverlessYml = {
+          service: 'new-service',
+          provider: {
+            name: 'aws',
+            stage: 'config-stage',
+          },
+          functions: {
+            first: {
+              events: [
+                {
+                  http: {
+                    path: 'foo',
+                    method: 'GET',
+                  },
+                },
+              ],
+            },
+          },
+        };
+        serverless.service = new serverless.classes.Service(serverless, serverlessYml);
+
+        expect(() => new AwsProvider(serverless, config)).to.throw(Error);
+      });
+    });
+
     describe('certificate authority - environment variable', () => {
       afterEach('Environment Variable Cleanup', () => {
         // clear env