Skip to content

Commit cdbd9b9

Browse files
author
Gabriel Monroy
committed
fix(scale): move initial scaling logic to model, add tests
1 parent 553978c commit cdbd9b9

5 files changed

Lines changed: 222 additions & 18 deletions

File tree

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
# -*- coding: utf-8 -*-
2+
from south.utils import datetime_utils as datetime
3+
from south.db import db
4+
from south.v2 import SchemaMigration
5+
from django.db import models
6+
7+
8+
class Migration(SchemaMigration):
9+
10+
def forwards(self, orm):
11+
# Removing unique constraint on 'Container', fields ['type', 'num']
12+
db.delete_unique(u'api_container', ['type', 'num'])
13+
14+
15+
def backwards(self, orm):
16+
# Adding unique constraint on 'Container', fields ['type', 'num']
17+
db.create_unique(u'api_container', ['type', 'num'])
18+
19+
20+
models = {
21+
u'api.app': {
22+
'Meta': {'object_name': 'App'},
23+
'cluster': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api.Cluster']"}),
24+
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
25+
'id': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '64'}),
26+
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
27+
'structure': ('json_field.fields.JSONField', [], {'default': "u'{}'", 'blank': 'True'}),
28+
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
29+
'uuid': ('api.fields.UuidField', [], {'unique': 'True', 'max_length': '32', 'primary_key': 'True'})
30+
},
31+
u'api.build': {
32+
'Meta': {'ordering': "[u'-created']", 'unique_together': "((u'app', u'uuid'),)", 'object_name': 'Build'},
33+
'app': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api.App']"}),
34+
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
35+
'dockerfile': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
36+
'image': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
37+
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
38+
'procfile': ('json_field.fields.JSONField', [], {'default': "u'{}'", 'blank': 'True'}),
39+
'sha': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}),
40+
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
41+
'uuid': ('api.fields.UuidField', [], {'unique': 'True', 'max_length': '32', 'primary_key': 'True'})
42+
},
43+
u'api.cluster': {
44+
'Meta': {'object_name': 'Cluster'},
45+
'auth': ('django.db.models.fields.TextField', [], {}),
46+
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
47+
'domain': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
48+
'hosts': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
49+
'id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'}),
50+
'options': ('json_field.fields.JSONField', [], {'default': "u'{}'", 'blank': 'True'}),
51+
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
52+
'type': ('django.db.models.fields.CharField', [], {'default': "u'coreos'", 'max_length': '16'}),
53+
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
54+
'uuid': ('api.fields.UuidField', [], {'unique': 'True', 'max_length': '32', 'primary_key': 'True'})
55+
},
56+
u'api.config': {
57+
'Meta': {'ordering': "[u'-created']", 'unique_together': "((u'app', u'uuid'),)", 'object_name': 'Config'},
58+
'app': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api.App']"}),
59+
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
60+
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
61+
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
62+
'uuid': ('api.fields.UuidField', [], {'unique': 'True', 'max_length': '32', 'primary_key': 'True'}),
63+
'values': ('json_field.fields.JSONField', [], {'default': "u'{}'", 'blank': 'True'})
64+
},
65+
u'api.container': {
66+
'Meta': {'ordering': "[u'created']", 'object_name': 'Container'},
67+
'app': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api.App']"}),
68+
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
69+
'num': ('django.db.models.fields.PositiveIntegerField', [], {}),
70+
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
71+
'release': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api.Release']"}),
72+
'state': ('django_fsm.FSMField', [], {'default': "u'initialized'", 'max_length': '50'}),
73+
'type': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}),
74+
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
75+
'uuid': ('api.fields.UuidField', [], {'unique': 'True', 'max_length': '32', 'primary_key': 'True'})
76+
},
77+
u'api.key': {
78+
'Meta': {'unique_together': "((u'owner', u'id'),)", 'object_name': 'Key'},
79+
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
80+
'id': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
81+
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
82+
'public': ('django.db.models.fields.TextField', [], {'unique': 'True'}),
83+
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
84+
'uuid': ('api.fields.UuidField', [], {'unique': 'True', 'max_length': '32', 'primary_key': 'True'})
85+
},
86+
u'api.push': {
87+
'Meta': {'ordering': "[u'-created']", 'unique_together': "((u'app', u'uuid'),)", 'object_name': 'Push'},
88+
'app': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api.App']"}),
89+
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
90+
'fingerprint': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
91+
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
92+
'receive_repo': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
93+
'receive_user': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
94+
'sha': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
95+
'ssh_connection': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
96+
'ssh_original_command': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
97+
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
98+
'uuid': ('api.fields.UuidField', [], {'unique': 'True', 'max_length': '32', 'primary_key': 'True'})
99+
},
100+
u'api.release': {
101+
'Meta': {'ordering': "[u'-created']", 'unique_together': "((u'app', u'version'),)", 'object_name': 'Release'},
102+
'app': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api.App']"}),
103+
'build': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api.Build']"}),
104+
'config': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api.Config']"}),
105+
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
106+
'image': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
107+
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
108+
'summary': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
109+
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
110+
'uuid': ('api.fields.UuidField', [], {'unique': 'True', 'max_length': '32', 'primary_key': 'True'}),
111+
'version': ('django.db.models.fields.PositiveIntegerField', [], {})
112+
},
113+
u'auth.group': {
114+
'Meta': {'object_name': 'Group'},
115+
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
116+
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
117+
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
118+
},
119+
u'auth.permission': {
120+
'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
121+
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
122+
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
123+
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
124+
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
125+
},
126+
u'auth.user': {
127+
'Meta': {'object_name': 'User'},
128+
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
129+
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
130+
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
131+
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
132+
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
133+
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
134+
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
135+
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
136+
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
137+
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
138+
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
139+
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
140+
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
141+
},
142+
u'contenttypes.contenttype': {
143+
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
144+
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
145+
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
146+
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
147+
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
148+
}
149+
}
150+
151+
complete_apps = ['api']

controller/api/models.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,18 @@ def delete(self, *args, **kwargs):
127127
c.destroy()
128128
return super(App, self).delete(*args, **kwargs)
129129

130-
def deploy(self, release):
130+
def deploy(self, release, initial=False):
131131
tasks.deploy_release.delay(self, release).get()
132+
# TODO: figure out if the logic below is what we really want
133+
if initial:
134+
# if there is procfile with a web worker, scale by web=1
135+
if release.build.procfile and 'web' in release.build.procfile:
136+
self.structure = {'web': 1}
137+
# otherwise assume dockerfile, scale cmd=1
138+
else:
139+
release.build.app.structure = {'cmd': 1}
140+
self.save()
141+
self.scale()
132142

133143
def destroy(self, *args, **kwargs):
134144
return self.delete(*args, **kwargs)

controller/api/tests/test_build.py

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
from __future__ import unicode_literals
88

99
import json
10-
import unittest
1110

1211
from django.test import TransactionTestCase
1312
from django.test.utils import override_settings
@@ -45,10 +44,6 @@ def test_build(self):
4544
response = self.client.get(url)
4645
self.assertEqual(response.status_code, 200)
4746
self.assertEqual(response.data['count'], 1)
48-
# TODO: the next test section seems to break `make test`.
49-
# See https://github.com/deis/deis/issues/727
50-
raise unittest.SkipTest(
51-
"Breaks database cleanup, see https://github.com/deis/deis/issues/727")
5247
# post a new build
5348
body = {'image': 'autotest/example'}
5449
response = self.client.post(url, json.dumps(body), content_type='application/json')
@@ -76,6 +71,46 @@ def test_build(self):
7671
self.assertEqual(self.client.patch(url).status_code, 405)
7772
self.assertEqual(self.client.delete(url).status_code, 405)
7873

74+
def test_build_default_containers(self):
75+
url = '/api/apps'
76+
body = {'cluster': 'autotest'}
77+
response = self.client.post(url, json.dumps(body), content_type='application/json')
78+
self.assertEqual(response.status_code, 201)
79+
app_id = response.data['id']
80+
# post a new build
81+
url = "/api/apps/{app_id}/builds".format(**locals())
82+
body = {'image': 'autotest/example'}
83+
response = self.client.post(url, json.dumps(body), content_type='application/json')
84+
self.assertEqual(response.status_code, 201)
85+
# test default container
86+
url = "/api/apps/{app_id}/containers/cmd".format(**locals())
87+
response = self.client.get(url)
88+
self.assertEqual(response.status_code, 200)
89+
self.assertEqual(len(response.data['results']), 1)
90+
container = response.data['results'][0]
91+
self.assertEqual(container['type'], 'cmd')
92+
self.assertEqual(container['num'], 1)
93+
# start with a new app
94+
url = '/api/apps'
95+
body = {'cluster': 'autotest'}
96+
response = self.client.post(url, json.dumps(body), content_type='application/json')
97+
self.assertEqual(response.status_code, 201)
98+
app_id = response.data['id']
99+
# post a new build with procfile
100+
url = "/api/apps/{app_id}/builds".format(**locals())
101+
body = {'image': 'autotest/example', 'procfile': json.dumps({'web': 'node server.js',
102+
'worker': 'node worker.js'})}
103+
response = self.client.post(url, json.dumps(body), content_type='application/json')
104+
self.assertEqual(response.status_code, 201)
105+
# test listing/retrieving container info
106+
url = "/api/apps/{app_id}/containers/web".format(**locals())
107+
response = self.client.get(url)
108+
self.assertEqual(response.status_code, 200)
109+
self.assertEqual(len(response.data['results']), 1)
110+
container = response.data['results'][0]
111+
self.assertEqual(container['type'], 'web')
112+
self.assertEqual(container['num'], 1)
113+
79114
def test_build_str(self):
80115
"""Test the text representation of a build."""
81116
url = '/api/apps'

controller/api/tests/test_hooks.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,14 @@ def test_build_hook_procfile(self):
156156
build = response.data['results'][0]
157157
self.assertEqual(build['sha'], SHA)
158158
self.assertEqual(build['procfile'], json.dumps(PROCFILE))
159+
# test listing/retrieving container info
160+
url = "/api/apps/{app_id}/containers/web".format(**locals())
161+
response = self.client.get(url)
162+
self.assertEqual(response.status_code, 200)
163+
self.assertEqual(len(response.data['results']), 1)
164+
container = response.data['results'][0]
165+
self.assertEqual(container['type'], 'web')
166+
self.assertEqual(container['num'], 1)
159167

160168
def test_build_hook_dockerfile(self):
161169
"""Test creating a Dockerfile build via an API Hook"""
@@ -197,6 +205,14 @@ def test_build_hook_dockerfile(self):
197205
build = response.data['results'][0]
198206
self.assertEqual(build['sha'], SHA)
199207
self.assertEqual(build['dockerfile'], DOCKERFILE)
208+
# test default container
209+
url = "/api/apps/{app_id}/containers/cmd".format(**locals())
210+
response = self.client.get(url)
211+
self.assertEqual(response.status_code, 200)
212+
self.assertEqual(len(response.data['results']), 1)
213+
container = response.data['results'][0]
214+
self.assertEqual(container['type'], 'cmd')
215+
self.assertEqual(container['num'], 1)
200216

201217
def test_config_hook(self):
202218
"""Test reading Config via an API Hook"""

controller/api/views.py

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -344,17 +344,8 @@ def post_save(self, build, created=False):
344344
if created:
345345
release = build.app.release_set.latest()
346346
self.release = release.new(self.request.user, build=build)
347-
build.app.deploy(self.release)
348-
# if the structure is empty (first build)
349-
if build.app.structure == {}:
350-
# if there is procfile with a web worker, scale by web=1
351-
if 'web' in build.procfile:
352-
build.app.structure = {'web': 1}
353-
# otherwise assume dockerfile, scale cmd=1
354-
else:
355-
build.app.structure = {'cmd': 1}
356-
build.app.save()
357-
build.app.scale()
347+
initial = True if build.app.structure == {} else False
348+
build.app.deploy(self.release, initial=initial)
358349

359350
def get_success_headers(self, data):
360351
headers = super(AppBuildViewSet, self).get_success_headers(data)
@@ -521,7 +512,8 @@ def post_save(self, build, created=False):
521512
if created:
522513
release = build.app.release_set.latest()
523514
new_release = release.new(build.owner, build=build)
524-
build.app.deploy(new_release)
515+
initial = True if build.app.structure == {} else False
516+
build.app.deploy(new_release, initial=initial)
525517

526518

527519
class ConfigHookViewSet(BaseHookViewSet):

0 commit comments

Comments
 (0)