Skip to content

Commit 5d6f00a

Browse files
author
Matthew Fisher
committed
Merge pull request #2116 from bacongobbler/apiv1
API v1 fixes
2 parents f1b3834 + 2b8aa07 commit 5d6f00a

10 files changed

Lines changed: 126 additions & 107 deletions

File tree

builder/templates/builder

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,8 @@ RESPONSE=$(curl -s -XPOST \
9292
--data "{\"receive_user\":\"$USER\",\"receive_repo\":\"$APP_NAME\"}" \
9393
$URL)
9494

95-
# massage response for the environment variables in form HELLO=world
96-
CONFIG=$(echo $RESPONSE | python -c 'import json,sys;obj=json.load(sys.stdin);print obj["values"]' | jq -c -M "to_entries|map(\"\(.key)=\(.value|tostring)\")|.[]" 2> /dev/null || echo $RESPONSE)
95+
# massage response for the environment variables in the form "-e HELLO=world"
96+
CONFIG=$(echo $RESPONSE | jq -r ".values|to_entries|map(\"-e \(.key)=\(.value|tostring)\")|.[]" | sed -e 's/^"//' -e 's/"$//' 2> /dev/null || echo $RESPONSE)
9797

9898
if [ "$CONFIG" == "$RESPONSE" ];
9999
then
@@ -102,32 +102,25 @@ then
102102
exit 1
103103
fi
104104

105-
# build option string to send to slugbuilder
106-
BUILD_OPTS=""
107-
echo $CONFIG > tmp
108-
while read envvar; do
109-
if [ "$envvar" != "" ]; then
110-
BUILD_OPTS+="-e $envvar "
111-
fi
112-
done < tmp
113-
rm tmp
114-
115105
# if no Dockerfile is present, use slugbuilder to compile a heroku slug
116106
# and write out a Dockerfile to use that slug
117107
if [ ! -f Dockerfile ]; then
118-
if [ -f /buildpacks ]; then
119-
BUILD_OPTS+="-v /buildpacks:/tmp/buildpacks:rw "
120-
# give non-root slubuilder user R/W perms for docker volumes
121-
chown -R 2000:2000 /buildpacks
122-
fi
108+
# build option string to send to slugbuilder
109+
BUILD_OPTS=("$CONFIG")
123110

124111
# run in the background, we'll attach to it to retrieve logs
125-
BUILD_OPTS+="-d "
126-
BUILD_OPTS+="-v $TMP_DIR:/tmp/app "
127-
BUILD_OPTS+="-v $CACHE_DIR:/tmp/cache:rw "
128-
# give non-root slubuilder user R/W perms for docker volumes
112+
BUILD_OPTS+=" -d"
113+
BUILD_OPTS+=" -v $TMP_DIR:/tmp/app"
114+
BUILD_OPTS+=" -v $CACHE_DIR:/tmp/cache:rw"
115+
# give non-root slugbuilder user R/W perms for docker volumes
129116
chown -R 2000:2000 $TMP_DIR $CACHE_DIR
130117

118+
if [ -f /buildpacks ]; then
119+
BUILD_OPTS+=" -v /buildpacks:/tmp/buildpacks:rw"
120+
# give non-root slugbuilder user R/W perms for docker volumes
121+
chown -R 2000:2000 /buildpacks
122+
fi
123+
131124
# build the application and attach to the process
132125
JOB=$(docker run $BUILD_OPTS deis/slugbuilder)
133126
docker attach $JOB

client/deis.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -621,7 +621,7 @@ def apps_logs(self, args):
621621
app = args.get('--app')
622622
if not app:
623623
app = self._session.app
624-
response = self._dispatch('post',
624+
response = self._dispatch('get',
625625
"/api/apps/{}/logs".format(app))
626626
if response.status_code == requests.codes.ok: # @UndefinedVariable
627627
# strip the last newline character
@@ -1125,7 +1125,7 @@ def config_list(self, args):
11251125
response = self._dispatch('get', "/api/apps/{}/config".format(app))
11261126
if response.status_code == requests.codes.ok: # @UndefinedVariable
11271127
config = response.json()
1128-
values = json.loads(config['values'])
1128+
values = config['values']
11291129
self._logger.info("=== {} Config".format(app))
11301130
items = values.items()
11311131
if len(items) == 0:
@@ -1180,7 +1180,7 @@ def config_set(self, args):
11801180
version = response.headers['x-deis-release']
11811181
self._logger.info("done, v{}\n".format(version))
11821182
config = response.json()
1183-
values = json.loads(config['values'])
1183+
values = config['values']
11841184
self._logger.info("=== {}".format(app))
11851185
items = values.items()
11861186
if len(items) == 0:
@@ -1226,7 +1226,7 @@ def config_unset(self, args):
12261226
version = response.headers['x-deis-release']
12271227
self._logger.info("done, v{}\n".format(version))
12281228
config = response.json()
1229-
values = json.loads(config['values'])
1229+
values = config['values']
12301230
self._logger.info("=== {}".format(app))
12311231
items = values.items()
12321232
if len(items) == 0:
@@ -1270,7 +1270,7 @@ def config_pull(self, args):
12701270
pass
12711271
response = self._dispatch('get', "/api/apps/{}/config".format(app))
12721272
if response.status_code == requests.codes.ok: # @UndefinedVariable
1273-
config = json.loads(response.json()['values'])
1273+
config = response.json()['values']
12741274
for k, v in config.items():
12751275
if interactive and raw_input("overwrite {} with {}? (y/N) ".format(k, v)) == 'y':
12761276
env_dict[k] = v
@@ -1545,9 +1545,9 @@ def write(d):
15451545
self._logger.info(("{k:<" + str(width) + "} {v}").format(**locals()))
15461546

15471547
self._logger.info("\n--- Memory")
1548-
write(json.loads(config.get('memory', '{}')))
1548+
write(config.get('memory', '{}'))
15491549
self._logger.info("\n--- CPU")
1550-
write(json.loads(config.get('cpu', '{}')))
1550+
write(config.get('cpu', '{}'))
15511551

15521552
def ps(self, args):
15531553
"""
@@ -1743,7 +1743,7 @@ def tags_unset(self, args):
17431743
raise ResponseError(response)
17441744

17451745
def _print_tags(self, app, config):
1746-
items = json.loads(config['tags'])
1746+
items = config['tags']
17471747
self._logger.info("=== {} Tags".format(app))
17481748
if len(items) == 0:
17491749
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: 23 additions & 6 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']
@@ -79,13 +79,13 @@ def test_app_actions(self):
7979
if os.path.exists(path):
8080
os.remove(path)
8181
url = '/api/apps/{app_id}/logs'.format(**locals())
82-
response = self.client.post(url)
82+
response = self.client.get(url)
8383
self.assertEqual(response.status_code, 204)
8484
self.assertEqual(response.data, 'No logs for {}'.format(app_id))
8585
# write out some fake log data and try again
8686
with open(path, 'a') as f:
8787
f.write(FAKE_LOG_DATA)
88-
response = self.client.post(url)
88+
response = self.client.get(url)
8989
self.assertEqual(response.status_code, 200)
9090
self.assertEqual(response.data, FAKE_LOG_DATA)
9191
os.remove(path)
@@ -107,7 +107,7 @@ def test_app_release_notes_in_logs(self):
107107
app_id = response.data['id'] # noqa
108108
path = os.path.join(settings.DEIS_LOG_DIR, app_id + '.log')
109109
url = '/api/apps/{app_id}/logs'.format(**locals())
110-
response = self.client.post(url)
110+
response = self.client.get(url)
111111
self.assertIn('autotest created initial release', response.data)
112112
self.assertEqual(response.status_code, 200)
113113
# delete file for future runs
@@ -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
"""
@@ -153,7 +170,7 @@ def test_admin_can_manage_other_apps(self):
153170
self.assertEqual(response.status_code, 200)
154171
# check app logs
155172
url = '/api/apps/{app_id}/logs'.format(**locals())
156-
response = self.client.post(url)
173+
response = self.client.get(url)
157174
self.assertEqual(response.status_code, 200)
158175
self.assertIn('autotest2 created initial release', response.data)
159176
# run one-off commands

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)