Repository: Miserlou/Zappa

Issue:
AWS API Error: Stage name only allows a-zA-Z0-9_
When using a stage name with a hyphen, I got the following:

```
$ zappa deploy v1-dev
Packaging project as zip...
Uploading api-v1-dev-1474761635.zip (4.2MiB)...
100%|████████████████████████████████| 4.39M/4.39M [00:04<00:00, 846KB/s]
Uploading api-v1-dev-template-1474761644.json (335.3KiB)...
100%|██████████████████████████████████| 343K/343K [00:00<00:00, 495KB/s]
Waiting for stack api-v1-dev to create (this can take a bit)...
100%|███████████████████████████████████| 64/64 [02:07<00:00,  3.82s/res]
Deploying API Gateway..
Oh no! An error occurred! :(

==============

Traceback (most recent call last):
  File "/Users/sean/src/virtualenvs/bottlestock-api/lib/python2.7/site-packages/zappa/cli.py", line 1288, in handle
    sys.exit(cli.handle())
  File "/Users/sean/src/virtualenvs/bottlestock-api/lib/python2.7/site-packages/zappa/cli.py", line 187, in handle
    self.deploy()
  File "/Users/sean/src/virtualenvs/bottlestock-api/lib/python2.7/site-packages/zappa/cli.py", line 324, in deploy
    cloudwatch_metrics_enabled=self.zappa_settings[self.api_stage].get('cloudwatch_metrics_enabled', False),
  File "/Users/sean/src/virtualenvs/bottlestock-api/lib/python2.7/site-packages/zappa/zappa.py", line 823, in deploy_api_gateway
    variables=variables or {}
  File "/Users/sean/src/virtualenvs/bottlestock-api/lib/python2.7/site-packages/botocore/client.py", line 159, in _api_call
    return self._make_api_call(operation_name, kwargs)
  File "/Users/sean/src/virtualenvs/bottlestock-api/lib/python2.7/site-packages/botocore/client.py", line 494, in _make_api_call
    raise ClientError(parsed_response, operation_name)
ClientError: An error occurred (BadRequestException) when calling the CreateDeployment operation: Stage name only allows a-zA-Z0-9_

==============

Need help? Found a bug? Let us know! :D
File bug reports on GitHub here: https://github.com/Miserlou/Zappa
And join our Slack channel here: http://bit.do/zappa
Love!,
 ~ Team Zappa!
```

This doesn't seem to be well documented, but the API error message does include the necessary pattern (and it doesn't include `-`).

I have a patch for this. It checks the stage name both when it's entered (on `init`) and when the settings are loaded. Will submit a PR.

AWS API Error: Stage name only allows a-zA-Z0-9_
When using a stage name with a hyphen, I got the following:

```
$ zappa deploy v1-dev
Packaging project as zip...
Uploading api-v1-dev-1474761635.zip (4.2MiB)...
100%|████████████████████████████████| 4.39M/4.39M [00:04<00:00, 846KB/s]
Uploading api-v1-dev-template-1474761644.json (335.3KiB)...
100%|██████████████████████████████████| 343K/343K [00:00<00:00, 495KB/s]
Waiting for stack api-v1-dev to create (this can take a bit)...
100%|███████████████████████████████████| 64/64 [02:07<00:00,  3.82s/res]
Deploying API Gateway..
Oh no! An error occurred! :(

==============

Traceback (most recent call last):
  File "/Users/sean/src/virtualenvs/bottlestock-api/lib/python2.7/site-packages/zappa/cli.py", line 1288, in handle
    sys.exit(cli.handle())
  File "/Users/sean/src/virtualenvs/bottlestock-api/lib/python2.7/site-packages/zappa/cli.py", line 187, in handle
    self.deploy()
  File "/Users/sean/src/virtualenvs/bottlestock-api/lib/python2.7/site-packages/zappa/cli.py", line 324, in deploy
    cloudwatch_metrics_enabled=self.zappa_settings[self.api_stage].get('cloudwatch_metrics_enabled', False),
  File "/Users/sean/src/virtualenvs/bottlestock-api/lib/python2.7/site-packages/zappa/zappa.py", line 823, in deploy_api_gateway
    variables=variables or {}
  File "/Users/sean/src/virtualenvs/bottlestock-api/lib/python2.7/site-packages/botocore/client.py", line 159, in _api_call
    return self._make_api_call(operation_name, kwargs)
  File "/Users/sean/src/virtualenvs/bottlestock-api/lib/python2.7/site-packages/botocore/client.py", line 494, in _make_api_call
    raise ClientError(parsed_response, operation_name)
ClientError: An error occurred (BadRequestException) when calling the CreateDeployment operation: Stage name only allows a-zA-Z0-9_

==============

Need help? Found a bug? Let us know! :D
File bug reports on GitHub here: https://github.com/Miserlou/Zappa
And join our Slack channel here: http://bit.do/zappa
Love!,
 ~ Team Zappa!
```

This doesn't seem to be well documented, but the API error message does include the necessary pattern (and it doesn't include `-`).

I have a patch for this. It checks the stage name both when it's entered (on `init`) and when the settings are loaded. Will submit a PR.



Discussion:

Resolving patch:
diff --git a/zappa/cli.py b/zappa/cli.py
--- a/zappa/cli.py
+++ b/zappa/cli.py
@@ -23,6 +23,7 @@
 import os
 import pkg_resources
 import random
+import re
 import requests
 import slugify
 import string
@@ -104,6 +105,8 @@ class ZappaCLI(object):
     exception_handler = None
     environment_variables = None
 
+    stage_name_env_pattern = re.compile('^[a-zA-Z0-9_]+$')
+
     @property
     def stage_config(self):
         """A shortcut property for settings of staging."""
@@ -178,7 +181,11 @@ def handle(self, argv=None):
                 self.app_function = vargs['app_function']
 
             # Load our settings
-            self.load_settings(vargs['settings_file'])
+            try:
+                self.load_settings(vargs['settings_file'])
+            except ValueError as e:
+                print("Error: {}".format(e.message))
+                sys.exit(-1)
             self.callback('settings')
 
         # Hand it off
@@ -441,7 +448,7 @@ def update(self):
             endpoint_url = 'https://' + endpoint_url
 
         deployed_string = "Your updated Zappa deployment is " + click.style("live", fg='green', bold=True) + "!: " + click.style("{}".format(endpoint_url), bold=True)
-        
+
         api_url = None
         if endpoint_url and 'amazonaws.com' not in endpoint_url:
             api_url = self.zappa.get_api_url(
@@ -759,6 +766,20 @@ def print_version(self): # pragma: no cover
         version = pkg_resources.require("zappa")[0].version
         print(version)
 
+    def check_stage_name(self, stage_name):
+        """
+        Make sure the stage name matches the AWS-allowed pattern
+
+        (calls to apigateway_client.create_deployment, will fail with error
+        message "ClientError: An error occurred (BadRequestException) when
+        calling the CreateDeployment operation: Stage name only allows
+        a-zA-Z0-9_" if the pattern does not match)
+        """
+        if self.stage_name_env_pattern.match(stage_name):
+            return True
+        raise ValueError("AWS requires stage name to match a-zA-Z0-9_")
+
+
     def init(self, settings_file="zappa_settings.json"):
         """
         Initialize a new Zappa project by creating a new zappa_settings.json in a guided process.
@@ -798,8 +819,14 @@ def init(self, settings_file="zappa_settings.json"):
         click.echo("Let's get started!\n")
 
         # Create Env
-        click.echo("Your Zappa configuration can support multiple production environments, like '" + click.style("dev", bold=True)  + "', '" + click.style("staging", bold=True)  + "', and '" + click.style("production", bold=True)  + "'.")
-        env = raw_input("What do you want to call this environment (default 'dev'): ") or "dev"
+        while True:
+            click.echo("Your Zappa configuration can support multiple production environments, like '" + click.style("dev", bold=True)  + "', '" + click.style("staging", bold=True)  + "', and '" + click.style("production", bold=True)  + "'.")
+            env = raw_input("What do you want to call this environment (default 'dev'): ") or "dev"
+            try:
+                self.check_stage_name(env)
+                break
+            except ValueError:
+                click.echo(click.style("Environment names must match a-zA-Z0-9_", fg='red'))
 
         # Create Bucket
         click.echo("\nYour Zappa deployments will need to be uploaded to a " + click.style("private S3 bucket", bold=True)  + ".")
@@ -997,6 +1024,13 @@ def load_settings(self, settings_file="zappa_settings.json", session=None):
         # Load up file
         self.load_settings_file(settings_file)
 
+        # Make sure that the environments are valid names:
+        for stage_name in self.zappa_settings.keys():
+            try:
+                self.check_stage_name(stage_name)
+            except ValueError:
+                raise ValueError("API stage names must match a-zA-Z0-9_ ; '{0!s}' does not.".format(stage_name))
+
         # Make sure that this environment is our settings
         if self.api_stage not in self.zappa_settings.keys():
             print("Please define '{0!s}' in your Zappa settings.".format(self.api_stage))


Test patch:
diff --git a/tests/test_bad_stage_name_settings.json b/tests/test_bad_stage_name_settings.json
new file mode 100644
--- /dev/null
+++ b/tests/test_bad_stage_name_settings.json
@@ -0,0 +1,48 @@
+{
+    "ttt-888": {
+       "touch": false,
+       "s3_bucket": "lmbda",
+       "app_function": "tests.test_app.hello_world",
+       "callbacks": {
+           "settings": "test_settings.callback",
+           "post": "test_settings.callback",
+           "zip": "test_settings.callback"
+       },
+       "delete_local_zip": true,
+       "debug": true,
+       "parameter_depth": 2, 
+       "prebuild_script": "test_settings.prebuild_me",
+       "events": [
+           {
+               "function": "tests.test_app.schedule_me",
+               "expression": "rate(1 minute)"
+           },
+           {
+               "function": "test.test_app.method",
+               "event_source": {
+                   "arn": "arn:aws:sns:::1",
+                   "events": [
+                     "sns:Publish"
+                   ]
+               }
+           }
+       ]
+    },
+    "devor": {
+       "s3_bucket": "lmbda",
+       "app_function": "tests.test_app.hello_world",
+       "callbacks": {
+           "settings": "test_settings.callback",
+           "post": "test_settings.callback",
+           "zip": "test_settings.callback"
+       },
+       "delete_local_zip": true,
+       "debug": true,
+       "parameter_depth": 2, 
+       "prebuild_script": "test_settings.prebuild_me",
+       "events": [{
+          "function": "tests.test_app.schedule_me",
+          "expression": "rate(1 minute)"
+       }]
+    }
+}
diff --git a/tests/tests.py b/tests/tests.py
--- a/tests/tests.py
+++ b/tests/tests.py
@@ -541,6 +541,10 @@ def test_bad_json_catch(self):
         zappa_cli = ZappaCLI()
         self.assertRaises(ValueError, zappa_cli.load_settings_file('tests/test_bad_settings.json'))
 
+    def test_bad_stage_name_catch(self):
+        zappa_cli = ZappaCLI()
+        self.assertRaises(ValueError, zappa_cli.load_settings, 'tests/test_bad_stage_name_settings.json')
+
     @placebo_session
     def test_cli_aws(self, session):
         zappa_cli = ZappaCLI()