Skip to content

Commit 2725d56

Browse files
author
Matthew Fisher
committed
Merge pull request #2941 from bacongobbler/2512-return-json-errors
fix(controller): return proper json messages on error
2 parents b2372c0 + 2a56376 commit 2725d56

9 files changed

Lines changed: 43 additions & 23 deletions

File tree

controller/api/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
The **api** Django app presents a RESTful web API for interacting with the **deis** system.
33
"""
44

5-
__version__ = '1.1.1'
5+
__version__ = '1.1.2'

controller/api/tests/test_app.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,8 @@ def test_run_without_auth(self):
247247
response = self.client.post(url, json.dumps(body), content_type='application/json',
248248
HTTP_AUTHORIZATION='token {}'.format(self.token))
249249
self.assertEquals(response.status_code, 400)
250-
self.assertEquals(response.data, 'Support for admin commands is not configured')
250+
self.assertEquals(response.data, {'detail': 'Support for admin commands '
251+
'is not configured'})
251252

252253
def test_run_without_release_should_error(self):
253254
"""
@@ -264,8 +265,8 @@ def test_run_without_release_should_error(self):
264265
response = self.client.post(url, json.dumps(body), content_type='application/json',
265266
HTTP_AUTHORIZATION='token {}'.format(self.token))
266267
self.assertEqual(response.status_code, 400)
267-
self.assertEqual(response.data, "No build associated with this release "
268-
"to run this command")
268+
self.assertEqual(response.data, {'detail': 'No build associated with this '
269+
'release to run this command'})
269270

270271
def test_unauthorized_user_cannot_see_app(self):
271272
"""

controller/api/tests/test_auth.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,8 @@ def test_passwd(self):
133133
response = self.client.post(url, json.dumps(submit), content_type='application/json',
134134
HTTP_AUTHORIZATION='token {}'.format(token))
135135
self.assertEqual(response.status_code, 400)
136+
self.assertEqual(response.data, {'detail': 'Current password does not match'})
137+
self.assertEqual(response.get('content-type'), 'application/json')
136138
submit = {
137139
'password': password,
138140
'new_password': 'password2',

controller/api/tests/test_container.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,8 @@ def test_container_errors(self):
303303
body = {'web': 'not_an_int'}
304304
response = self.client.post(url, json.dumps(body), content_type='application/json',
305305
HTTP_AUTHORIZATION='token {}'.format(self.token))
306-
self.assertContains(response, 'Invalid scaling format', status_code=400)
306+
self.assertEqual(response.status_code, 400)
307+
self.assertEqual(response.data, {'detail': 'Invalid scaling format'})
307308
body = {'invalid': 1}
308309
response = self.client.post(url, json.dumps(body), content_type='application/json',
309310
HTTP_AUTHORIZATION='token {}'.format(self.token))
@@ -446,7 +447,7 @@ def test_scale_without_build_should_error(self):
446447
response = self.client.post(url, json.dumps(body), content_type='application/json',
447448
HTTP_AUTHORIZATION='token {}'.format(self.token))
448449
self.assertEqual(response.status_code, 400)
449-
self.assertEqual(response.data, "No build associated with this release")
450+
self.assertEqual(response.data, {'detail': 'No build associated with this release'})
450451

451452
def test_command_good(self):
452453
"""Test the default command for each container workflow"""

controller/api/tests/test_release.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,8 @@ def test_release_rollback(self):
147147
response = self.client.post(url, content_type='application/json',
148148
HTTP_AUTHORIZATION='token {}'.format(self.token))
149149
self.assertEqual(response.status_code, 400)
150+
self.assertEqual(response.data, {'detail': 'version cannot be below 0'})
151+
self.assertEqual(response.get('content-type'), 'application/json')
150152
# update config to roll a new release
151153
url = '/v1/apps/{app_id}/config'.format(**locals())
152154
body = {'values': json.dumps({'NEW_URL1': 'http://localhost:8080/'})}

controller/api/tests/test_scheduler.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ def test_create_chaos(self):
6969
response = self.client.post(url, json.dumps(body), content_type='application/json',
7070
HTTP_AUTHORIZATION='token {}'.format(self.token))
7171
self.assertEqual(response.status_code, 503)
72+
self.assertEqual(response.data, {'detail': 'aborting, failed to create some containers'})
73+
self.assertEqual(response.get('content-type'), 'application/json')
7274
# inspect broken containers
7375
url = "/v1/apps/{app_id}/containers".format(**locals())
7476
response = self.client.get(url, HTTP_AUTHORIZATION='token {}'.format(self.token))
@@ -151,6 +153,8 @@ def test_destroy_chaos(self):
151153
response = self.client.post(url, json.dumps(body), content_type='application/json',
152154
HTTP_AUTHORIZATION='token {}'.format(self.token))
153155
self.assertEqual(response.status_code, 503)
156+
self.assertEqual(response.data, {'detail': 'aborting, failed to destroy some containers'})
157+
self.assertEqual(response.get('content-type'), 'application/json')
154158
# inspect broken containers
155159
url = "/v1/apps/{app_id}/containers".format(**locals())
156160
response = self.client.get(url, HTTP_AUTHORIZATION='token {}'.format(self.token))
@@ -167,7 +171,10 @@ def test_destroy_chaos(self):
167171
# break if we destroyed successfully
168172
if response.status_code == 204:
169173
break
170-
self.assertEquals(response.status_code, 503)
174+
self.assertEqual(response.status_code, 503)
175+
self.assertEqual(response.data, {'detail': 'aborting, failed to '
176+
'destroy some containers'})
177+
self.assertEqual(response.get('content-type'), 'application/json')
171178
# inspect broken containers
172179
url = "/v1/apps/{app_id}/containers".format(**locals())
173180
response = self.client.get(url, HTTP_AUTHORIZATION='token {}'.format(self.token))
@@ -216,6 +223,8 @@ def test_build_chaos(self):
216223
response = self.client.post(url, json.dumps(body), content_type='application/json',
217224
HTTP_AUTHORIZATION='token {}'.format(self.token))
218225
self.assertEqual(response.status_code, 503)
226+
self.assertEqual(response.data, {'detail': 'aborting, failed to create some containers'})
227+
self.assertEqual(response.get('content-type'), 'application/json')
219228
# inspect releases
220229
url = "/v1/apps/{app_id}/releases".format(**locals())
221230
response = self.client.get(url, HTTP_AUTHORIZATION='token {}'.format(self.token))
@@ -267,6 +276,8 @@ def test_config_chaos(self):
267276
response = self.client.post(url, json.dumps(body), content_type='application/json',
268277
HTTP_AUTHORIZATION='token {}'.format(self.token))
269278
self.assertEqual(response.status_code, 503)
279+
self.assertEqual(response.data, {'detail': 'aborting, failed to create some containers'})
280+
self.assertEqual(response.get('content-type'), 'application/json')
270281
# inspect releases
271282
url = "/v1/apps/{app_id}/releases".format(**locals())
272283
response = self.client.get(url, HTTP_AUTHORIZATION='token {}'.format(self.token))
@@ -315,3 +326,5 @@ def test_run_chaos(self):
315326
response = self.client.post(url, json.dumps(body), content_type='application/json',
316327
HTTP_AUTHORIZATION='token {}'.format(self.token))
317328
self.assertEqual(response.status_code, 503)
329+
self.assertEqual(response.data, {'detail': 'exit code 1'})
330+
self.assertEqual(response.get('content-type'), 'application/json')

controller/api/views.py

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ def get_object(self):
3838
def passwd(self, request, **kwargs):
3939
obj = self.get_object()
4040
if not obj.check_password(request.DATA['password']):
41-
return Response("Current password did not match", status=status.HTTP_400_BAD_REQUEST)
41+
return Response({'detail': 'Current password does not match'},
42+
status=status.HTTP_400_BAD_REQUEST)
4243
obj.set_password(request.DATA['new_password'])
4344
obj.save()
4445
return Response({'status': 'password set'})
@@ -60,7 +61,7 @@ def create(self, request, *args, **kwargs):
6061
return super(BaseDeisViewSet, self).create(request, *args, **kwargs)
6162
# If the scheduler oopsie'd
6263
except RuntimeError as e:
63-
return Response(str(e), status=status.HTTP_503_SERVICE_UNAVAILABLE)
64+
return Response({'detail': str(e)}, status=status.HTTP_503_SERVICE_UNAVAILABLE)
6465

6566

6667
class AppResourceViewSet(BaseDeisViewSet):
@@ -131,22 +132,20 @@ def post_save(self, app):
131132

132133
def scale(self, request, **kwargs):
133134
new_structure = {}
135+
app = self.get_object()
134136
try:
135137
for target, count in request.DATA.items():
136138
new_structure[target] = int(count)
137-
except (TypeError, ValueError):
138-
return Response('Invalid scaling format',
139-
status=status.HTTP_400_BAD_REQUEST)
140-
app = self.get_object()
141-
try:
142139
models.validate_app_structure(new_structure)
143140
app.scale(request.user, new_structure)
144-
except (EnvironmentError, ValidationError) as e:
145-
return Response(str(e), status=status.HTTP_400_BAD_REQUEST)
141+
except (TypeError, ValueError):
142+
return Response({'detail': 'Invalid scaling format'},
143+
status=status.HTTP_400_BAD_REQUEST)
144+
except (ValidationError, EnvironmentError) as e:
145+
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
146146
except RuntimeError as e:
147-
return Response(str(e), status=status.HTTP_503_SERVICE_UNAVAILABLE)
148-
return Response(status=status.HTTP_204_NO_CONTENT,
149-
content_type='application/json')
147+
return Response({'detail': str(e)}, status=status.HTTP_503_SERVICE_UNAVAILABLE)
148+
return Response(status=status.HTTP_204_NO_CONTENT)
150149

151150
def logs(self, request, **kwargs):
152151
app = self.get_object()
@@ -165,9 +164,9 @@ def run(self, request, **kwargs):
165164
try:
166165
output_and_rc = app.run(self.request.user, command)
167166
except EnvironmentError as e:
168-
return Response(str(e), status=status.HTTP_400_BAD_REQUEST)
167+
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
169168
except RuntimeError as e:
170-
return Response(str(e), status=status.HTTP_503_SERVICE_UNAVAILABLE)
169+
return Response({'detail': str(e)}, status=status.HTTP_503_SERVICE_UNAVAILABLE)
171170
return Response(output_and_rc, status=status.HTTP_200_OK,
172171
content_type='text/plain')
173172

@@ -252,7 +251,7 @@ def rollback(self, request, **kwargs):
252251
response = {'version': new_release.version}
253252
return Response(response, status=status.HTTP_201_CREATED)
254253
except EnvironmentError as e:
255-
return Response(str(e), status=status.HTTP_400_BAD_REQUEST)
254+
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
256255
except RuntimeError:
257256
new_release.delete()
258257
raise

controller/scheduler/chaos.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def run(self, name, image, entrypoint, command):
5050
Run a one-off command
5151
"""
5252
if random.random() < CREATE_ERROR_RATE:
53-
raise RuntimeError
53+
raise RuntimeError('exit code 1')
5454
return 0, ''
5555

5656
SchedulerClient = ChaosSchedulerClient

docs/reference/api-v1.1.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ What's New
1818

1919
**New!** Users see a 404 response when modifying applications they are unauthorized to see.
2020

21+
**New!** Responses returned from the scheduler are properly formatted JSON responses.
22+
2123

2224
Authentication
2325
--------------

0 commit comments

Comments
 (0)