Skip to content

Commit 6ccd7e0

Browse files
committed
Merge pull request #1658 from mboersma/1557-validate-ps-scale
fix(controller): validate app structure values
2 parents 6685120 + 1a53a73 commit 6ccd7e0

4 files changed

Lines changed: 61 additions & 3 deletions

File tree

controller/api/models.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,16 @@ def validate_comma_separated(value):
6161
"{} should be a comma-separated list".format(value))
6262

6363

64+
def validate_app_structure(value):
65+
"""Error if the dict values aren't ints >= 0."""
66+
try:
67+
for k, v in value.iteritems():
68+
if int(v) < 0:
69+
raise ValueError("Must be greater than or equal to zero")
70+
except ValueError, err:
71+
raise ValidationError(err)
72+
73+
6474
class AuditedModel(models.Model):
6575
"""Add created and updated fields to a model."""
6676

@@ -134,7 +144,7 @@ class App(UuidAuditedModel):
134144
owner = models.ForeignKey(settings.AUTH_USER_MODEL)
135145
id = models.SlugField(max_length=64, unique=True)
136146
cluster = models.ForeignKey('Cluster')
137-
structure = JSONField(default={}, blank=True)
147+
structure = JSONField(default={}, blank=True, validators=[validate_app_structure])
138148

139149
class Meta:
140150
permissions = (('use_app', 'Can use app'),)

controller/api/serializers.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,14 @@ def validate_id(self, attrs, source):
184184
raise serializers.ValidationError("App IDs cannot be 'deis'")
185185
return attrs
186186

187+
def validate_structure(self, attrs, source):
188+
"""
189+
Check that the structure JSON dict has non-negative ints as its values.
190+
"""
191+
value = attrs[source]
192+
models.validate_app_structure(value)
193+
return attrs
194+
187195

188196
class ContainerSerializer(serializers.ModelSerializer):
189197
"""Serialize a :class:`~api.models.Container` model."""

controller/api/tests/test_container.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,3 +362,41 @@ def test_container_command_format(self):
362362
uuid = response.data['results'][0]['uuid']
363363
container = Container.objects.get(uuid=uuid)
364364
self.assertNotIn('{c_type}', container._command)
365+
366+
def test_container_scale_errors(self):
367+
url = '/api/apps'
368+
body = {'cluster': 'autotest'}
369+
response = self.client.post(url, json.dumps(body), content_type='application/json')
370+
self.assertEqual(response.status_code, 201)
371+
app_id = response.data['id']
372+
# should start with zero
373+
url = "/api/apps/{app_id}/containers".format(**locals())
374+
response = self.client.get(url)
375+
self.assertEqual(response.status_code, 200)
376+
self.assertEqual(len(response.data['results']), 0)
377+
# post a new build
378+
url = "/api/apps/{app_id}/builds".format(**locals())
379+
body = {'image': 'autotest/example', 'sha': 'a'*40,
380+
'procfile': json.dumps({'web': 'node server.js', 'worker': 'node worker.js'})}
381+
response = self.client.post(url, json.dumps(body), content_type='application/json')
382+
self.assertEqual(response.status_code, 201)
383+
# scale to a negative number
384+
url = "/api/apps/{app_id}/scale".format(**locals())
385+
body = {'web': -1}
386+
response = self.client.post(url, json.dumps(body), content_type='application/json')
387+
self.assertEqual(response.status_code, 400)
388+
# scale to something other than a number
389+
url = "/api/apps/{app_id}/scale".format(**locals())
390+
body = {'web': 'one'}
391+
response = self.client.post(url, json.dumps(body), content_type='application/json')
392+
self.assertEqual(response.status_code, 400)
393+
# scale to something other than a number
394+
url = "/api/apps/{app_id}/scale".format(**locals())
395+
body = {'web': [1]}
396+
response = self.client.post(url, json.dumps(body), content_type='application/json')
397+
self.assertEqual(response.status_code, 400)
398+
# scale up to an integer as a sanity check
399+
url = "/api/apps/{app_id}/scale".format(**locals())
400+
body = {'web': 1}
401+
response = self.client.post(url, json.dumps(body), content_type='application/json')
402+
self.assertEqual(response.status_code, 204)

controller/api/views.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from django.contrib.auth.models import AnonymousUser
1010
from django.contrib.auth.models import User
11+
from django.core.exceptions import ValidationError
1112
from django.http import Http404
1213
from django.utils import timezone
1314
from guardian.shortcuts import assign_perm
@@ -282,14 +283,15 @@ def scale(self, request, **kwargs):
282283
try:
283284
for target, count in request.DATA.items():
284285
new_structure[target] = int(count)
285-
except ValueError:
286+
except (TypeError, ValueError):
286287
return Response('Invalid scaling format',
287288
status=status.HTTP_400_BAD_REQUEST)
288289
app = self.get_object()
289290
try:
291+
models.validate_app_structure(new_structure)
290292
app.structure = new_structure
291293
app.scale()
292-
except EnvironmentError as e:
294+
except (EnvironmentError, ValidationError) as e:
293295
return Response(str(e), status=status.HTTP_400_BAD_REQUEST)
294296
return Response(status=status.HTTP_204_NO_CONTENT,
295297
content_type='application/json')

0 commit comments

Comments
 (0)