Skip to content

Commit 318d9b4

Browse files
author
Gabriel Monroy
committed
add container.port and make it unique across a formation for multi-app
1 parent f6f13b4 commit 318d9b4

3 files changed

Lines changed: 97 additions & 9 deletions

File tree

api/migrations/0001_initial.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,13 +131,17 @@ def forwards(self, orm):
131131
('app', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['api.App'])),
132132
('type', self.gf('django.db.models.fields.CharField')(max_length=128)),
133133
('num', self.gf('django.db.models.fields.PositiveIntegerField')()),
134+
('port', self.gf('django.db.models.fields.PositiveIntegerField')()),
134135
('status', self.gf('django.db.models.fields.CharField')(default=u'up', max_length=64)),
135136
))
136137
db.send_create_signal(u'api', ['Container'])
137138

138139
# Adding unique constraint on 'Container', fields ['app', 'type', 'num']
139140
db.create_unique(u'api_container', ['app_id', 'type', 'num'])
140141

142+
# Adding unique constraint on 'Container', fields ['formation', 'port']
143+
db.create_unique(u'api_container', ['formation_id', 'port'])
144+
141145
# Adding model 'Config'
142146
db.create_table(u'api_config', (
143147
('uuid', self.gf('api.fields.UuidField')(unique=True, max_length=32, primary_key=True)),
@@ -202,6 +206,9 @@ def backwards(self, orm):
202206
# Removing unique constraint on 'Config', fields ['app', 'version']
203207
db.delete_unique(u'api_config', ['app_id', 'version'])
204208

209+
# Removing unique constraint on 'Container', fields ['formation', 'port']
210+
db.delete_unique(u'api_container', ['formation_id', 'port'])
211+
205212
# Removing unique constraint on 'Container', fields ['app', 'type', 'num']
206213
db.delete_unique(u'api_container', ['app_id', 'type', 'num'])
207214

@@ -295,13 +302,14 @@ def backwards(self, orm):
295302
'version': ('django.db.models.fields.PositiveIntegerField', [], {})
296303
},
297304
u'api.container': {
298-
'Meta': {'ordering': "[u'created']", 'unique_together': "((u'app', u'type', u'num'),)", 'object_name': 'Container'},
305+
'Meta': {'ordering': "[u'created']", 'unique_together': "((u'app', u'type', u'num'), (u'formation', u'port'))", 'object_name': 'Container'},
299306
'app': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api.App']"}),
300307
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
301308
'formation': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api.Formation']"}),
302309
'node': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api.Node']"}),
303310
'num': ('django.db.models.fields.PositiveIntegerField', [], {}),
304311
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
312+
'port': ('django.db.models.fields.PositiveIntegerField', [], {}),
305313
'status': ('django.db.models.fields.CharField', [], {'default': "u'up'", 'max_length': '64'}),
306314
'type': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
307315
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),

api/models.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -218,14 +218,13 @@ def calculate(self):
218218
d['apps'][a.id]['proxy']['port'] = 80
219219
d['apps'][a.id]['proxy']['backends'] = []
220220
d['apps'][a.id]['containers'] = containers = {}
221-
for c in self.container_set.all().order_by('created'):
222-
port = 5000 + c.num
221+
for c in a.container_set.all().order_by('created'):
223222
containers.setdefault(c.type, {})
224223
containers[c.type].update(
225-
{c.num: "{0}:{1}".format(c.node.id, port)})
224+
{c.num: "{0}:{1}".format(c.node.id, c.port)})
226225
if c.type == 'web':
227226
d['apps'][a.id]['proxy']['backends'].append(
228-
"{0}:{1}".format(c.node.fqdn, port))
227+
"{0}:{1}".format(c.node.fqdn, c.port))
229228
return d
230229

231230

@@ -363,6 +362,12 @@ def next_runtime_node(self, formation, container_type, reverse=False):
363362
count.reverse()
364363
return count[0][1]
365364

365+
def next_runtime_port(self, formation):
366+
containers = Container.objects.filter(formation=formation).order_by('-port')
367+
if not containers:
368+
return 10001
369+
return containers[0].port + 1
370+
366371

367372
@python_2_unicode_compatible
368373
class Node(UuidAuditedModel):
@@ -554,12 +559,14 @@ def scale(self, app, structure, **kwargs):
554559
while diff > 0:
555560
# get the next node with the fewest containers
556561
node = Node.objects.next_runtime_node(formation, container_type)
562+
port = Node.objects.next_runtime_port(formation)
557563
c = Container.objects.create(owner=app.owner,
558564
formation=formation,
559565
node=node,
560566
app=app,
561567
type=container_type,
562-
num=container_num)
568+
num=container_num,
569+
port=port)
563570
containers.append(c)
564571
container_num += 1
565572
diff -= 1
@@ -600,7 +607,8 @@ def balance(self, formation, **kwargs):
600607
app=app,
601608
type=container_type,
602609
num=container_num,
603-
node=n_under)
610+
node=n_under,
611+
port=Node.objects.next_runtime_port(formation))
604612
container_num += 1
605613
# update the n_map accordingly
606614
for n in (n_over, n_under):
@@ -624,6 +632,7 @@ class Container(UuidAuditedModel):
624632
app = models.ForeignKey('App')
625633
type = models.CharField(max_length=128)
626634
num = models.PositiveIntegerField()
635+
port = models.PositiveIntegerField()
627636

628637
# TODO: add celery beat tasks for monitoring node health
629638
status = models.CharField(max_length=64, default='up')
@@ -638,7 +647,8 @@ def __str__(self):
638647
class Meta:
639648
get_latest_by = '-created'
640649
ordering = ['created']
641-
unique_together = (('app', 'type', 'num'),)
650+
unique_together = (('app', 'type', 'num'),
651+
('formation', 'port'))
642652

643653

644654
@python_2_unicode_compatible
@@ -713,7 +723,7 @@ def push(cls, push):
713723
release_signal.send(sender=push, build=new_build, app=app, user=user)
714724
# see if we need to scale an initial web container
715725
if len(app.formation.node_set.filter(layer__runtime=True)) > 0 and \
716-
len(app.formation.container_set.filter(type='web')) < 1:
726+
len(app.container_set.filter(type='web')) < 1:
717727
# scale an initial web containers
718728
Container.objects.scale(app, {'web': 1})
719729
# publish and converge the application

api/tests/container.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,76 @@ def test_container_single_layer(self):
201201
self.assertEqual(response.status_code, 200)
202202
self.assertEqual(response.data['containers'], json.dumps(body))
203203

204+
def test_container_multiple_apps(self):
205+
url = '/api/apps'
206+
body = {'formation': 'autotest'}
207+
response = self.client.post(url, json.dumps(body), content_type='application/json')
208+
self.assertEqual(response.status_code, 201)
209+
app1_id = response.data['id']
210+
response = self.client.post(url, json.dumps(body), content_type='application/json')
211+
self.assertEqual(response.status_code, 201)
212+
app2_id = response.data['id']
213+
# scale up
214+
url = "/api/apps/{app1_id}/scale".format(**locals())
215+
body = {'web': 4, 'worker': 2}
216+
response = self.client.post(url, json.dumps(body), content_type='application/json')
217+
self.assertEqual(response.status_code, 200)
218+
url = "/api/apps/{app2_id}/scale".format(**locals())
219+
body = {'web': 4, 'worker': 2}
220+
response = self.client.post(url, json.dumps(body), content_type='application/json')
221+
self.assertEqual(response.status_code, 200)
222+
url = "/api/apps/{app1_id}/containers".format(**locals())
223+
response = self.client.get(url)
224+
self.assertEqual(response.status_code, 200)
225+
self.assertEqual(len(response.data['results']), 6)
226+
url = "/api/apps/{app2_id}/containers".format(**locals())
227+
response = self.client.get(url)
228+
self.assertEqual(response.status_code, 200)
229+
self.assertEqual(len(response.data['results']), 6)
230+
# check port assignments
231+
url = '/api/formations/autotest/calculate'
232+
response = self.client.post(url)
233+
self.assertEqual(response.status_code, 200)
234+
databag = response.data.copy()
235+
ports = []
236+
for app in databag['apps'].values():
237+
for containers_set in app['containers'].values():
238+
for node_port in containers_set.values():
239+
_, port = node_port.split(':')
240+
ports.append(int(port))
241+
ports.sort()
242+
self.assertEqual(ports, range(10001, 10013))
243+
# scale down
244+
url = "/api/apps/{app1_id}/scale".format(**locals())
245+
body = {'web': 2, 'worker': 1}
246+
response = self.client.post(url, json.dumps(body), content_type='application/json')
247+
self.assertEqual(response.status_code, 200)
248+
url = "/api/apps/{app2_id}/scale".format(**locals())
249+
body = {'web': 2, 'worker': 1}
250+
response = self.client.post(url, json.dumps(body), content_type='application/json')
251+
self.assertEqual(response.status_code, 200)
252+
url = "/api/apps/{app1_id}/containers".format(**locals())
253+
response = self.client.get(url)
254+
self.assertEqual(response.status_code, 200)
255+
self.assertEqual(len(response.data['results']), 3)
256+
url = "/api/apps/{app2_id}/containers".format(**locals())
257+
response = self.client.get(url)
258+
self.assertEqual(response.status_code, 200)
259+
self.assertEqual(len(response.data['results']), 3)
260+
# check port assignments
261+
url = '/api/formations/autotest/calculate'
262+
response = self.client.post(url)
263+
self.assertEqual(response.status_code, 200)
264+
databag = response.data.copy()
265+
ports = []
266+
for app in databag['apps'].values():
267+
for containers_set in app['containers'].values():
268+
for node_port in containers_set.values():
269+
_, port = node_port.split(':')
270+
ports.append(int(port))
271+
ports.sort()
272+
self.assertEqual(len(set(ports)), 6)
273+
204274
def test_container_allocation(self):
205275
url = '/api/apps'
206276
formation_id = 'autotest'

0 commit comments

Comments
 (0)