Skip to content

Commit e310501

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 e9343e7 commit e310501

8 files changed

Lines changed: 104 additions & 79 deletions

File tree

client/deis.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1122,7 +1122,7 @@ def config_list(self, args):
11221122
response = self._dispatch('get', "/api/apps/{}/config".format(app))
11231123
if response.status_code == requests.codes.ok: # @UndefinedVariable
11241124
config = response.json()
1125-
values = json.loads(config['values'])
1125+
values = config['values']
11261126
self._logger.info("=== {} Config".format(app))
11271127
items = values.items()
11281128
if len(items) == 0:
@@ -1177,7 +1177,7 @@ def config_set(self, args):
11771177
version = response.headers['x-deis-release']
11781178
self._logger.info("done, v{}\n".format(version))
11791179
config = response.json()
1180-
values = json.loads(config['values'])
1180+
values = config['values']
11811181
self._logger.info("=== {}".format(app))
11821182
items = values.items()
11831183
if len(items) == 0:
@@ -1223,7 +1223,7 @@ def config_unset(self, args):
12231223
version = response.headers['x-deis-release']
12241224
self._logger.info("done, v{}\n".format(version))
12251225
config = response.json()
1226-
values = json.loads(config['values'])
1226+
values = config['values']
12271227
self._logger.info("=== {}".format(app))
12281228
items = values.items()
12291229
if len(items) == 0:
@@ -1267,7 +1267,7 @@ def config_pull(self, args):
12671267
pass
12681268
response = self._dispatch('get', "/api/apps/{}/config".format(app))
12691269
if response.status_code == requests.codes.ok: # @UndefinedVariable
1270-
config = json.loads(response.json()['values'])
1270+
config = response.json()['values']
12711271
for k, v in config.items():
12721272
if interactive and raw_input("overwrite {} with {}? (y/N) ".format(k, v)) == 'y':
12731273
env_dict[k] = v
@@ -1542,9 +1542,9 @@ def write(d):
15421542
self._logger.info(("{k:<" + str(width) + "} {v}").format(**locals()))
15431543

15441544
self._logger.info("\n--- Memory")
1545-
write(json.loads(config.get('memory', '{}')))
1545+
write(config.get('memory', '{}'))
15461546
self._logger.info("\n--- CPU")
1547-
write(json.loads(config.get('cpu', '{}')))
1547+
write(config.get('cpu', '{}'))
15481548

15491549
def ps(self, args):
15501550
"""
@@ -1740,7 +1740,7 @@ def tags_unset(self, args):
17401740
raise ResponseError(response)
17411741

17421742
def _print_tags(self, app, config):
1743-
items = json.loads(config['tags'])
1743+
items = config['tags']
17441744
self._logger.info("=== {} Tags".format(app))
17451745
if len(items) == 0:
17461746
self._logger.info('No tags defined')

controller/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."""

controller/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
"""

controller/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

0 commit comments

Comments
 (0)