Skip to content

Commit 570d034

Browse files
author
Matthew Fisher
committed
fix(controller): properly serialize JSONField objects
JSONField is a finnicky thing. When serializing the object in the API, it will just dump the object as a string. This ends in an invalid JSON response. In the case of the Config object: {"owner": "bacongobbler", "app": "go", "values": "{\"HELLO\": \"world\"}", "memory": "{}", "cpu": "{}", "tags": "{}", ...} This fix changes the JSON output to the following: {"owner": "bacongobbler", "app": "go", "values": {"HELLO": "world"}, "memory": {}, "cpu": {}, "tags": {}} This is completely backwards compatible with previous requests. The JSONSerializer will accept both strings and JSON objects in the request.
1 parent 1f64346 commit 570d034

7 files changed

Lines changed: 97 additions & 72 deletions

File tree

api/serializers.py

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from __future__ import unicode_literals
66

7+
import json
78
import re
89

910
from django.conf import settings
@@ -32,6 +33,18 @@ def from_native(self, data):
3233
return serializers.SlugRelatedField.from_native(self, data)
3334

3435

36+
class JSONFieldSerializer(serializers.WritableField):
37+
def to_native(self, obj):
38+
return obj
39+
40+
def from_native(self, value):
41+
try:
42+
val = json.loads(value)
43+
except TypeError:
44+
val = value
45+
return val
46+
47+
3548
class UserSerializer(serializers.ModelSerializer):
3649
"""Serialize a User model."""
3750

@@ -64,6 +77,7 @@ class ClusterSerializer(serializers.ModelSerializer):
6477
"""Serialize a :class:`~api.models.Cluster` model."""
6578

6679
owner = serializers.Field(source='owner.username')
80+
options = JSONFieldSerializer(source='options', required=False)
6781

6882
class Meta:
6983
"""Metadata options for a :class:`ClusterSerializer`."""
@@ -98,6 +112,7 @@ class BuildSerializer(serializers.ModelSerializer):
98112

99113
owner = serializers.Field(source='owner.username')
100114
app = serializers.SlugRelatedField(slug_field='id')
115+
procfile = JSONFieldSerializer(source='procfile', required=False)
101116

102117
class Meta:
103118
"""Metadata options for a :class:`BuildSerializer`."""
@@ -110,14 +125,10 @@ class ConfigSerializer(serializers.ModelSerializer):
110125

111126
owner = serializers.Field(source='owner.username')
112127
app = serializers.SlugRelatedField(slug_field='id')
113-
values = serializers.ModelField(
114-
model_field=models.Config()._meta.get_field('values'), required=False)
115-
memory = serializers.ModelField(
116-
model_field=models.Config()._meta.get_field('memory'), required=False)
117-
cpu = serializers.ModelField(
118-
model_field=models.Config()._meta.get_field('cpu'), required=False)
119-
tags = serializers.ModelField(
120-
model_field=models.Config()._meta.get_field('tags'), required=False)
128+
values = JSONFieldSerializer(source='values', required=False)
129+
memory = JSONFieldSerializer(source='memory', required=False)
130+
cpu = JSONFieldSerializer(source='cpu', required=False)
131+
tags = JSONFieldSerializer(source='tags', required=False)
121132

122133
class Meta:
123134
"""Metadata options for a :class:`ConfigSerializer`."""
@@ -185,6 +196,7 @@ class AppSerializer(serializers.ModelSerializer):
185196
id = serializers.SlugField(default=utils.generate_app_name)
186197
cluster = serializers.SlugRelatedField(slug_field='id')
187198
url = serializers.Field(source='url')
199+
structure = JSONFieldSerializer(source='structure', required=False)
188200

189201
class Meta:
190202
"""Metadata options for a :class:`AppSerializer`."""
@@ -203,14 +215,6 @@ def validate_id(self, attrs, source):
203215
raise serializers.ValidationError("App IDs cannot be 'deis'")
204216
return attrs
205217

206-
def validate_structure(self, attrs, source):
207-
"""
208-
Check that the structure JSON dict has non-negative ints as its values.
209-
"""
210-
value = attrs[source]
211-
models.validate_app_structure(value)
212-
return attrs
213-
214218

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

api/tests/test_app.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@
1010
import os.path
1111

1212
from django.test import TestCase
13-
1413
from django.conf import settings
1514

15+
from api.models import App
1616

17-
class AppTest(TestCase):
1817

18+
class AppTest(TestCase):
1919
"""Tests creation of applications"""
2020

2121
fixtures = ['tests.json']
@@ -135,6 +135,23 @@ def test_app_errors(self):
135135
response = self.client.get(url)
136136
self.assertEquals(response.status_code, 404)
137137

138+
def test_app_structure_is_valid_json(self):
139+
"""Application structures should be valid JSON objects."""
140+
url = '/api/apps'
141+
body = {'cluster': 'autotest'}
142+
response = self.client.post(url, json.dumps(body), content_type='application/json')
143+
self.assertEqual(response.status_code, 201)
144+
app_id = response.data['id']
145+
self.assertIn('structure', response.data)
146+
self.assertEqual(response.data['structure'], {})
147+
app = App.objects.get(id=app_id)
148+
app.structure = {'web': 1}
149+
app.save()
150+
url = '/api/apps/{}'.format(app_id)
151+
response = self.client.get(url)
152+
self.assertIn('structure', response.data)
153+
self.assertEqual(response.data['structure'], {"web": 1})
154+
138155
def test_admin_can_manage_other_apps(self):
139156
"""Administrators of Deis should be able to manage all applications.
140157
"""

api/tests/test_cluster.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def test_cluster(self):
4242
self.assertIn('auth', response.data)
4343
self.assertIn('options', response.data)
4444
self.assertEqual(response.data['hosts'], 'host1,host2')
45-
self.assertEqual(json.loads(response.data['options']), {'key': 'val'})
45+
self.assertEqual(response.data['options'], {'key': 'val'})
4646
response = self.client.get('/api/clusters')
4747
self.assertEqual(response.status_code, 200)
4848
self.assertEqual(len(response.data['results']), 1)
@@ -68,7 +68,7 @@ def test_cluster(self):
6868
response = self.client.patch(url, json.dumps(body), content_type='application/json')
6969
self.assertEqual(response.status_code, 200)
7070
self.assertEqual(response.data['hosts'], new_hosts)
71-
self.assertEqual(json.loads(response.data['options']), new_options)
71+
self.assertEqual(response.data['options'], new_options)
7272
response = self.client.delete(url)
7373
self.assertEqual(response.status_code, 204)
7474

api/tests/test_config.py

Lines changed: 30 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def test_config(self):
5454
response = self.client.get(url)
5555
self.assertEqual(response.status_code, 200)
5656
self.assertIn('values', response.data)
57-
self.assertEqual(response.data['values'], json.dumps({}))
57+
self.assertEqual(response.data['values'], {})
5858
config1 = response.data
5959
# set an initial config value
6060
body = {'values': json.dumps({'NEW_URL1': 'http://localhost:8080/'})}
@@ -63,28 +63,28 @@ def test_config(self):
6363
self.assertIn('x-deis-release', response._headers)
6464
config2 = response.data
6565
self.assertNotEqual(config1['uuid'], config2['uuid'])
66-
self.assertIn('NEW_URL1', json.loads(response.data['values']))
66+
self.assertIn('NEW_URL1', response.data['values'])
6767
# read the config
6868
response = self.client.get(url)
6969
self.assertEqual(response.status_code, 200)
7070
config3 = response.data
7171
self.assertEqual(config2, config3)
72-
self.assertIn('NEW_URL1', json.loads(response.data['values']))
72+
self.assertIn('NEW_URL1', response.data['values'])
7373
# set an additional config value
7474
body = {'values': json.dumps({'NEW_URL2': 'http://localhost:8080/'})}
7575
response = self.client.post(url, json.dumps(body), content_type='application/json')
7676
self.assertEqual(response.status_code, 201)
7777
config3 = response.data
7878
self.assertNotEqual(config2['uuid'], config3['uuid'])
79-
self.assertIn('NEW_URL1', json.loads(response.data['values']))
80-
self.assertIn('NEW_URL2', json.loads(response.data['values']))
79+
self.assertIn('NEW_URL1', response.data['values'])
80+
self.assertIn('NEW_URL2', response.data['values'])
8181
# read the config again
8282
response = self.client.get(url)
8383
self.assertEqual(response.status_code, 200)
8484
config4 = response.data
8585
self.assertEqual(config3, config4)
86-
self.assertIn('NEW_URL1', json.loads(response.data['values']))
87-
self.assertIn('NEW_URL2', json.loads(response.data['values']))
86+
self.assertIn('NEW_URL1', response.data['values'])
87+
self.assertIn('NEW_URL2', response.data['values'])
8888
# unset a config value
8989
body = {'values': json.dumps({'NEW_URL2': None})}
9090
response = self.client.post(url, json.dumps(body), content_type='application/json')
@@ -118,13 +118,13 @@ def test_config_set_same_key(self):
118118
body = {'values': json.dumps({'PORT': '5000'})}
119119
response = self.client.post(url, json.dumps(body), content_type='application/json')
120120
self.assertEqual(response.status_code, 201)
121-
self.assertIn('PORT', json.loads(response.data['values']))
121+
self.assertIn('PORT', response.data['values'])
122122
# reset same config value
123123
body = {'values': json.dumps({'PORT': '5001'})}
124124
response = self.client.post(url, json.dumps(body), content_type='application/json')
125125
self.assertEqual(response.status_code, 201)
126-
self.assertIn('PORT', json.loads(response.data['values']))
127-
self.assertEqual(json.loads(response.data['values'])['PORT'], '5001')
126+
self.assertIn('PORT', response.data['values'])
127+
self.assertEqual(response.data['values']['PORT'], '5001')
128128

129129
@mock.patch('requests.post', mock_import_repository_task)
130130
def test_config_set_unicode(self):
@@ -141,19 +141,19 @@ def test_config_set_unicode(self):
141141
body = {'values': json.dumps({'POWERED_BY': 'Деис'})}
142142
response = self.client.post(url, json.dumps(body), content_type='application/json')
143143
self.assertEqual(response.status_code, 201)
144-
self.assertIn('POWERED_BY', json.loads(response.data['values']))
144+
self.assertIn('POWERED_BY', response.data['values'])
145145
# reset same config value
146146
body = {'values': json.dumps({'POWERED_BY': 'Кроликов'})}
147147
response = self.client.post(url, json.dumps(body), content_type='application/json')
148148
self.assertEqual(response.status_code, 201)
149-
self.assertIn('POWERED_BY', json.loads(response.data['values']))
150-
self.assertEqual(json.loads(response.data['values'])['POWERED_BY'], 'Кроликов')
149+
self.assertIn('POWERED_BY', response.data['values'])
150+
self.assertEqual(response.data['values']['POWERED_BY'], 'Кроликов')
151151
# set an integer to test unicode regression
152152
body = {'values': json.dumps({'INTEGER': 1})}
153153
response = self.client.post(url, json.dumps(body), content_type='application/json')
154154
self.assertEqual(response.status_code, 201)
155-
self.assertIn('INTEGER', json.loads(response.data['values']))
156-
self.assertEqual(json.loads(response.data['values'])['INTEGER'], 1)
155+
self.assertIn('INTEGER', response.data['values'])
156+
self.assertEqual(response.data['values']['INTEGER'], 1)
157157

158158
@mock.patch('requests.post', mock_import_repository_task)
159159
def test_config_str(self):
@@ -179,7 +179,7 @@ def test_admin_can_create_config_on_other_apps(self):
179179
body = {'values': json.dumps({'PORT': '5000'})}
180180
response = self.client.post(url, json.dumps(body), content_type='application/json')
181181
self.assertEqual(response.status_code, 201)
182-
self.assertIn('PORT', json.loads(response.data['values']))
182+
self.assertIn('PORT', response.data['values'])
183183

184184
@mock.patch('requests.post', mock_import_repository_task)
185185
def test_limit_memory(self):
@@ -197,7 +197,7 @@ def test_limit_memory(self):
197197
response = self.client.get(url, content_type='application/json')
198198
self.assertEqual(response.status_code, 200)
199199
self.assertIn('memory', response.data)
200-
self.assertEqual(json.loads(response.data['memory']), {})
200+
self.assertEqual(response.data['memory'], {})
201201
# regression test for https://github.com/deis/deis/issues/1563
202202
self.assertNotIn('"', response.data['memory'])
203203
# set an initial limit
@@ -211,7 +211,7 @@ def test_limit_memory(self):
211211
response = self.client.get(url, content_type='application/json')
212212
self.assertEqual(response.status_code, 200)
213213
self.assertIn('memory', response.data)
214-
memory = json.loads(response.data['memory'])
214+
memory = response.data['memory']
215215
self.assertIn('web', memory)
216216
self.assertEqual(memory['web'], '1G')
217217
# set an additional value
@@ -220,7 +220,7 @@ def test_limit_memory(self):
220220
self.assertEqual(response.status_code, 201)
221221
limit2 = response.data
222222
self.assertNotEqual(limit1['uuid'], limit2['uuid'])
223-
memory = json.loads(response.data['memory'])
223+
memory = response.data['memory']
224224
self.assertIn('worker', memory)
225225
self.assertEqual(memory['worker'], '512M')
226226
self.assertIn('web', memory)
@@ -230,7 +230,7 @@ def test_limit_memory(self):
230230
self.assertEqual(response.status_code, 200)
231231
limit3 = response.data
232232
self.assertEqual(limit2, limit3)
233-
memory = json.loads(response.data['memory'])
233+
memory = response.data['memory']
234234
self.assertIn('worker', memory)
235235
self.assertEqual(memory['worker'], '512M')
236236
self.assertIn('web', memory)
@@ -240,11 +240,11 @@ def test_limit_memory(self):
240240
body = {'values': json.dumps({'NEW_URL2': 'http://localhost:8080/'})}
241241
response = self.client.post(url, json.dumps(body), content_type='application/json')
242242
self.assertEqual(response.status_code, 201)
243-
self.assertIn('NEW_URL2', json.loads(response.data['values']))
243+
self.assertIn('NEW_URL2', response.data['values'])
244244
# read the limit again
245245
response = self.client.get(url)
246246
self.assertEqual(response.status_code, 200)
247-
memory = json.loads(response.data['memory'])
247+
memory = response.data['memory']
248248
self.assertIn('worker', memory)
249249
self.assertEqual(memory['worker'], '512M')
250250
self.assertIn('web', memory)
@@ -277,7 +277,7 @@ def test_limit_cpu(self):
277277
response = self.client.get(url, content_type='application/json')
278278
self.assertEqual(response.status_code, 200)
279279
self.assertIn('cpu', response.data)
280-
self.assertEqual(json.loads(response.data['cpu']), {})
280+
self.assertEqual(response.data['cpu'], {})
281281
# regression test for https://github.com/deis/deis/issues/1563
282282
self.assertNotIn('"', response.data['cpu'])
283283
# set an initial limit
@@ -290,7 +290,7 @@ def test_limit_cpu(self):
290290
response = self.client.get(url, content_type='application/json')
291291
self.assertEqual(response.status_code, 200)
292292
self.assertIn('cpu', response.data)
293-
cpu = json.loads(response.data['cpu'])
293+
cpu = response.data['cpu']
294294
self.assertIn('web', cpu)
295295
self.assertEqual(cpu['web'], '1024')
296296
# set an additional value
@@ -299,7 +299,7 @@ def test_limit_cpu(self):
299299
self.assertEqual(response.status_code, 201)
300300
limit2 = response.data
301301
self.assertNotEqual(limit1['uuid'], limit2['uuid'])
302-
cpu = json.loads(response.data['cpu'])
302+
cpu = response.data['cpu']
303303
self.assertIn('worker', cpu)
304304
self.assertEqual(cpu['worker'], '512')
305305
self.assertIn('web', cpu)
@@ -309,7 +309,7 @@ def test_limit_cpu(self):
309309
self.assertEqual(response.status_code, 200)
310310
limit3 = response.data
311311
self.assertEqual(limit2, limit3)
312-
cpu = json.loads(response.data['cpu'])
312+
cpu = response.data['cpu']
313313
self.assertIn('worker', cpu)
314314
self.assertEqual(cpu['worker'], '512')
315315
self.assertIn('web', cpu)
@@ -342,7 +342,7 @@ def test_tags(self):
342342
response = self.client.get(url, content_type='application/json')
343343
self.assertEqual(response.status_code, 200)
344344
self.assertIn('tags', response.data)
345-
self.assertEqual(json.loads(response.data['tags']), {})
345+
self.assertEqual(response.data['tags'], {})
346346
# set some tags
347347
body = {'tags': json.dumps({'environ': 'dev'})}
348348
response = self.client.post(url, json.dumps(body), content_type='application/json')
@@ -353,7 +353,7 @@ def test_tags(self):
353353
response = self.client.get(url, content_type='application/json')
354354
self.assertEqual(response.status_code, 200)
355355
self.assertIn('tags', response.data)
356-
tags = json.loads(response.data['tags'])
356+
tags = response.data['tags']
357357
self.assertIn('environ', tags)
358358
self.assertEqual(tags['environ'], 'dev')
359359
# set an additional value
@@ -362,7 +362,7 @@ def test_tags(self):
362362
self.assertEqual(response.status_code, 201)
363363
tags2 = response.data
364364
self.assertNotEqual(tags1['uuid'], tags2['uuid'])
365-
tags = json.loads(response.data['tags'])
365+
tags = response.data['tags']
366366
self.assertIn('rack', tags)
367367
self.assertEqual(tags['rack'], '1')
368368
self.assertIn('environ', tags)
@@ -372,7 +372,7 @@ def test_tags(self):
372372
self.assertEqual(response.status_code, 200)
373373
tags3 = response.data
374374
self.assertEqual(tags2, tags3)
375-
tags = json.loads(response.data['tags'])
375+
tags = response.data['tags']
376376
self.assertIn('rack', tags)
377377
self.assertEqual(tags['rack'], '1')
378378
self.assertIn('environ', tags)

api/tests/test_hooks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ def test_build_hook_procfile(self):
163163
self.assertIn('results', response.data)
164164
build = response.data['results'][0]
165165
self.assertEqual(build['sha'], SHA)
166-
self.assertEqual(build['procfile'], json.dumps(PROCFILE))
166+
self.assertEqual(build['procfile'], PROCFILE)
167167
# test listing/retrieving container info
168168
url = "/api/apps/{app_id}/containers/web".format(**locals())
169169
response = self.client.get(url)

0 commit comments

Comments
 (0)