Repository: deis/deis

Issue:
Create formation without credentials results in 500
This should result in a `400 Bad Request` with appropriate error text.

```
$ deis formations:create dev --flavor=ec2-us-west-2
Creating formation... done, created dev
Creating runtime layer... 500 INTERNAL SERVER ERROR
<h1>Server Error (500)</h1>
```

Here is the stack trace on the server.

```
2014-01-27 14:03:30 [70] [DEBUG] POST /api/formations/dev/layers
ERROR Internal Server Error: /api/formations/dev/layers
Traceback (most recent call last):
  File "/usr/local/lib/python2.7/dist-packages/django/core/handlers/base.py", line 114, in get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/usr/local/lib/python2.7/dist-packages/rest_framework/viewsets.py", line 78, in view
    return self.dispatch(request, *args, **kwargs)
  File "/usr/local/lib/python2.7/dist-packages/django/views/decorators/csrf.py", line 57, in wrapped_view
    return view_func(*args, **kwargs)
  File "/usr/local/lib/python2.7/dist-packages/rest_framework/views.py", line 399, in dispatch
    response = self.handle_exception(exc)
  File "/usr/local/lib/python2.7/dist-packages/rest_framework/views.py", line 396, in dispatch
    response = handler(request, *args, **kwargs)
  File "/app/deis/api/views.py", line 320, in create
    return super(FormationLayerViewSet, self).create(request, **kwargs)
  File "/usr/local/lib/python2.7/dist-packages/rest_framework/mixins.py", line 55, in create
    self.post_save(self.object, created=True)
  File "/app/deis/api/views.py", line 324, in post_save
    layer.build()
  File "/app/deis/api/models.py", line 304, in build
    return tasks.build_layer.delay(self).wait()
  File "/usr/local/lib/python2.7/dist-packages/celery/result.py", line 151, in get
    interval=interval)
  File "/usr/local/lib/python2.7/dist-packages/celery/backends/base.py", line 177, in wait_for
    raise result
EnvironmentError: No credentials provided
```



Discussion:
Alternatively, the server should just try to proactively authenticate the user and return `401 Unauthorized` if the token is rejected before any server-side code is run.

To clarify, I'm referring to missing EC2 credentials. In the case of a bad/missing session token, we already return a `401 Unauthorized` at the Django REST Framework level.

Sorry to go slightly off topic again. For production would it be a good idea to catch all API exceptions with the rest framework's custom `EXCEPTION_HANDLER` function? http://www.django-rest-framework.org/api-guide/exceptions


Resolving patch:
diff --git a/api/exceptions.py b/api/exceptions.py
--- a/api/exceptions.py
+++ b/api/exceptions.py
@@ -20,3 +20,17 @@ class BuildNodeError(APIException):
 
     def __init__(self, detail=None):
         self.detail = detail
+
+
+class BuildFormationError(APIException):
+    """
+    Indicates a problem in creating a formation.
+
+    This exception is subclassed from rest_framework's APIException so it
+    isn't reported as "500 SERVER ERROR."
+    """
+
+    status_code = status.HTTP_400_BAD_REQUEST
+
+    def __init__(self, detail=None):
+        self.detail = detail
diff --git a/api/views.py b/api/views.py
--- a/api/views.py
+++ b/api/views.py
@@ -24,6 +24,7 @@
 from rest_framework.response import Response
 
 from api import docker, models, serializers
+from .exceptions import BuildFormationError
 
 from deis import settings
 
@@ -318,11 +319,17 @@ def create(self, request, **kwargs):
 
     def post_save(self, layer, created=False, **kwargs):
         if created:
-            layer.build()
+            try:
+                layer.build()
+            except EnvironmentError as err:
+                raise BuildFormationError(str(err))
 
     def destroy(self, request, **kwargs):
         layer = self.get_object()
-        layer.destroy()
+        try:
+            layer.destroy()
+        except EnvironmentError as err:
+            raise BuildFormationError(str(err))
         return Response(status=status.HTTP_204_NO_CONTENT)
 
 


Test patch:
diff --git a/api/tests/test_layer.py b/api/tests/test_layer.py
--- a/api/tests/test_layer.py
+++ b/api/tests/test_layer.py
@@ -58,6 +58,12 @@ def test_layer(self):
         self.assertIn('runtime', response.data)
         self.assertIn('config', response.data)
         self.assertIn('key', json.loads(response.data['config']))
+        # test layer build failure
+        url = '/api/formations/{formation_id}/layers'.format(**locals())
+        body = {'id': 'autotest-fail', 'flavor': 'autotest-fail',
+                'config': json.dumps({'key': 'value'})}
+        response = self.client.post(url, json.dumps(body), content_type='application/json')
+        self.assertEqual(response.status_code, 400)
         url = '/api/formations/{formation_id}/layers'.format(**locals())
         response = self.client.get(url)
         self.assertEqual(response.status_code, 200)