Repository: serverless/serverless (JavaScript)
Date: 2019-01-11 23:17:11+00:00
Task type: Bug Fix

Issue:
If stage name is a variable then you can not create an http binding
<!--
1. If you have a question and not a bug report please ask first at http://forum.serverless.com
2. Please check if an issue already exists. This bug may have already been documented
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

## Description

* What went wrong?
I deployed my sls stack and got the following error:
`Serverless plugin "/lib/plugins/aws/provider/awsProvider.js" initialization errored: Invalid stage name ${self:custom.env}: it should contains only [a-zA-Z0-9] for AWS provider if http event are present since API Gateway stage name cannot contains hyphens.`
* What did you expect should have happened?
A successful deploy
* What was the config you used?
```
provider:
  name: aws
  stage: ${self:custom.env}
...
functions:
  get_transactions:
    events:
      - http:
        ...
```
when ${self:custom.env} resolves to dev
* What stacktrace or error message from your provider did you see?
`lib/plugins/aws/provider/awsProvider.js" initialization errored: Invalid stage name ${self:custom.env}: it should contains only [a-zA-Z0-9] for AWS provider if http event are present since API Gateway stage name cannot contains hyphens.`
Similar or dependent issues:
* #3528

## Additional Data

* ***Serverless Framework Version you're using***:
1.35.1
* ***Operating System***:
osx
* ***Stack Trace***:
NA
* ***Provider Error messages***:
lib/plugins/aws/provider/awsProvider.js" initialization errored: Invalid stage name ${self:custom.env}: it should contains only [a-zA-Z0-9] for AWS provider if http event are present since API Gateway stage name cannot contains hyphens.`


Issue discussion:
Hmm. That's odd. I'm not able to reproduce this issue. Could you share a full `serverless.yml` that causes the issue for me to try?

Here's the `serverless.yml` I used:
```yaml
service: sls-5655 # NOTE: update this with your service name
custom:
  stage: custom-stage
provider:
  name: aws
  runtime: nodejs8.10
  stage: ${self:custom.stage}
functions:
  hello:
    handler: handler.hello
```

And here's the output when I run `sls deploy`:
```
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Creating Stack...
Serverless: Checking Stack create progress...
.....
Serverless: Stack create finished...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service .zip file to S3 (387 B)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
................
Serverless: Stack update finished...
Service Information
service: sls-5655
stage: custom-stage
region: us-east-1
stack: sls-5655-custom-stage
api keys:
  None
endpoints:
  None
functions:
  hello: sls-5655-custom-stage-hello
layers:
  None
```

and here's the output of `sls print`
```yaml
service: sls-5655
custom:
  stage: custom-stage
provider:
  stage: custom-stage
  name: aws
  runtime: nodejs8.10
functions:
  hello:
    handler: handler.hello
```
@dschep you need to define an http event.

For example:
```
service:
  name: test-stack

custom:
  env: test

provider:
  name: aws
  stage: ${self:custom.env}
  runtime: nodejs8.10

functions:
  tracerTest:
    memorySize: 512
    handler: tracer-test.handler
    events:
      - http:
          method: get
          path: whatever
```
Gotcha. I see the releavnt pr too now. Thanks :)

Solution patch:
diff --git a/lib/classes/Variables.js b/lib/classes/Variables.js
index 764ed2a006e..ad13e3c422d 100644
--- a/lib/classes/Variables.js
+++ b/lib/classes/Variables.js
@@ -10,6 +10,8 @@ const fse = require('../utils/fs/fse');
 const logWarning = require('./Error').logWarning;
 const PromiseTracker = require('./PromiseTracker');
 
+const validAPIGatewayStageNamePattern = /^[-_a-zA-Z0-9]+$/;
+
 /**
  * Maintainer's notes:
  *
@@ -108,6 +110,19 @@ class Variables {
           this.populateValue(config.value, true) // populate
             .then(populated => _.assign(config, { populated })));
         return this.assignProperties(provider, prepopulations);
+      }).then(() => {
+        const stage = provider.getStage();
+        this.serverless.service.getAllFunctions().forEach(funcName => {
+          _.forEach(this.serverless.service.getAllEventsInFunction(funcName), event => {
+            if (_.has(event, 'http') && !validAPIGatewayStageNamePattern.test(stage)) {
+              throw new this.serverless.classes.Error([
+                `Invalid stage name ${stage}:`,
+                'it should contains only [-_a-zA-Z0-9] for AWS provider if http event are present',
+                'according to API Gateway limitation.',
+              ].join(' '));
+            }
+          });
+        });
       });
     }
     return BbPromise.resolve();
diff --git a/lib/plugins/aws/provider/awsProvider.js b/lib/plugins/aws/provider/awsProvider.js
index cb68ddcd898..bb9df211560 100644
--- a/lib/plugins/aws/provider/awsProvider.js
+++ b/lib/plugins/aws/provider/awsProvider.js
@@ -19,8 +19,6 @@ const constants = {
   providerName: 'aws',
 };
 
-const validAPIGatewayStageNamePattern = /^[a-zA-Z0-9_]+$/;
-
 PromiseQueue.configure(BbPromise.Promise);
 
 const impl = {
@@ -201,19 +199,6 @@ 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/classes/Variables.test.js b/lib/classes/Variables.test.js
index e486855ed64..eb69f8a624f 100644
--- a/lib/classes/Variables.test.js
+++ b/lib/classes/Variables.test.js
@@ -207,6 +207,113 @@ describe('Variables', () => {
         });
       });
     });
+    describe('stage name validation', () => {
+      it(`should not throw an error if http event is absent and 
+            stage contains only alphanumeric, underscore and hyphen`, () => {
+        const serverlessYml = {
+          service: 'new-service',
+          provider: {
+            name: 'aws',
+            stage: 'xyz-101_abc-123',
+          },
+          functions: {
+            first: {
+              events: [],
+            },
+          },
+        };
+        serverless.service = new serverless.classes.Service(serverless, serverlessYml);
+
+        return serverless.variables.populateService().should.be.fullfilled;
+      });
+
+      it(`should not throw an error after variable population if http event is present and
+            the populated stage contains only alphanumeric, underscore and hyphen`, () => {
+        const serverlessYml = {
+          service: 'new-service',
+          provider: {
+            name: 'aws',
+            stage: '${opt:stage, "default-stage"}',
+          },
+          functions: {
+            first: {
+              events: [
+                {
+                  http: {
+                    path: 'foo',
+                    method: 'GET',
+                  },
+                },
+              ],
+            },
+          },
+        };
+        serverless.service = new serverless.classes.Service(serverless, serverlessYml);
+
+        return serverless.variables.populateService().should.be.fullfilled;
+      });
+
+      it('should throw an error if http event is present and stage contains invalid chars', () => {
+        const serverlessYml = {
+          service: 'new-service',
+          provider: {
+            name: 'aws',
+            stage: 'my@stage',
+          },
+          functions: {
+            first: {
+              events: [
+                {
+                  http: {
+                    path: 'foo',
+                    method: 'GET',
+                  },
+                },
+              ],
+            },
+          },
+        };
+        serverless.service = new serverless.classes.Service(serverless, serverlessYml);
+
+        return serverless.variables.populateService().should.be
+          .rejectedWith(serverless.classes.Error, [
+            'Invalid stage name my@stage: it should contains only [-_a-zA-Z0-9]',
+            'for AWS provider if http event are present',
+            'according to API Gateway limitation.',
+          ].join(' '));
+      });
+
+      it(`should throw an error after variable population
+            if http event is present and stage contains hyphen`, () => {
+        const serverlessYml = {
+          service: 'new-service',
+          provider: {
+            name: 'aws',
+            stage: '${opt:stage, "default:stage"}',
+          },
+          functions: {
+            first: {
+              events: [
+                {
+                  http: {
+                    path: 'foo',
+                    method: 'GET',
+                  },
+                },
+              ],
+            },
+          },
+        };
+        serverless.service = new serverless.classes.Service(serverless, serverlessYml);
+
+        return serverless.variables.populateService().should.be
+          .rejectedWith(serverless.classes.Error, [
+            'Invalid stage name default:stage: it should contains only [-_a-zA-Z0-9]',
+            'for AWS provider if http event are present',
+            'according to API Gateway limitation.',
+          ].join(' '));
+      });
+    });
   });
 
   describe('#getProperties', () => {
diff --git a/lib/plugins/aws/provider/awsProvider.test.js b/lib/plugins/aws/provider/awsProvider.test.js
index d3ce0ee9227..b2dc452212e 100644
--- a/lib/plugins/aws/provider/awsProvider.test.js
+++ b/lib/plugins/aws/provider/awsProvider.test.js
@@ -90,51 +90,43 @@ 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);
+    describe('stage name validation', () => {
+      const stages = [
+        'myStage',
+        'my-stage',
+        'my_stage',
+        '${opt:stage, \'prod\'}',
+      ];
+      stages.forEach(stage => {
+        it(`should not throw an error before variable population 
+            even if http event is present and stage is ${stage}`, () => {
+          const 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',
+          const serverlessYml = {
+            service: 'new-service',
+            provider: {
+              name: 'aws',
+              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);
+          };
+          serverless.service = new serverless.classes.Service(serverless, serverlessYml);
+          expect(() => new AwsProvider(serverless, config)).to.not.throw(Error);
+        });
       });
     });