Skip to content
This repository has been archived by the owner on Nov 30, 2021. It is now read-only.

Commit

Permalink
fix(scale): move initial scaling logic to model, add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Gabriel Monroy committed May 16, 2014
1 parent 553978c commit cdbd9b9
Show file tree
Hide file tree
Showing 5 changed files with 222 additions and 18 deletions.
151 changes: 151 additions & 0 deletions controller/api/migrations/0011_auto__del_unique_container_type_num.py
@@ -0,0 +1,151 @@
# -*- coding: utf-8 -*-
from south.utils import datetime_utils as datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models


class Migration(SchemaMigration):

def forwards(self, orm):
# Removing unique constraint on 'Container', fields ['type', 'num']
db.delete_unique(u'api_container', ['type', 'num'])


def backwards(self, orm):
# Adding unique constraint on 'Container', fields ['type', 'num']
db.create_unique(u'api_container', ['type', 'num'])


models = {
u'api.app': {
'Meta': {'object_name': 'App'},
'cluster': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api.Cluster']"}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '64'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
'structure': ('json_field.fields.JSONField', [], {'default': "u'{}'", 'blank': 'True'}),
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'uuid': ('api.fields.UuidField', [], {'unique': 'True', 'max_length': '32', 'primary_key': 'True'})
},
u'api.build': {
'Meta': {'ordering': "[u'-created']", 'unique_together': "((u'app', u'uuid'),)", 'object_name': 'Build'},
'app': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api.App']"}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'dockerfile': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'image': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
'procfile': ('json_field.fields.JSONField', [], {'default': "u'{}'", 'blank': 'True'}),
'sha': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}),
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'uuid': ('api.fields.UuidField', [], {'unique': 'True', 'max_length': '32', 'primary_key': 'True'})
},
u'api.cluster': {
'Meta': {'object_name': 'Cluster'},
'auth': ('django.db.models.fields.TextField', [], {}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'domain': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'hosts': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
'id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'}),
'options': ('json_field.fields.JSONField', [], {'default': "u'{}'", 'blank': 'True'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
'type': ('django.db.models.fields.CharField', [], {'default': "u'coreos'", 'max_length': '16'}),
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'uuid': ('api.fields.UuidField', [], {'unique': 'True', 'max_length': '32', 'primary_key': 'True'})
},
u'api.config': {
'Meta': {'ordering': "[u'-created']", 'unique_together': "((u'app', u'uuid'),)", 'object_name': 'Config'},
'app': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api.App']"}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'uuid': ('api.fields.UuidField', [], {'unique': 'True', 'max_length': '32', 'primary_key': 'True'}),
'values': ('json_field.fields.JSONField', [], {'default': "u'{}'", 'blank': 'True'})
},
u'api.container': {
'Meta': {'ordering': "[u'created']", 'object_name': 'Container'},
'app': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api.App']"}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'num': ('django.db.models.fields.PositiveIntegerField', [], {}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
'release': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api.Release']"}),
'state': ('django_fsm.FSMField', [], {'default': "u'initialized'", 'max_length': '50'}),
'type': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}),
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'uuid': ('api.fields.UuidField', [], {'unique': 'True', 'max_length': '32', 'primary_key': 'True'})
},
u'api.key': {
'Meta': {'unique_together': "((u'owner', u'id'),)", 'object_name': 'Key'},
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
'public': ('django.db.models.fields.TextField', [], {'unique': 'True'}),
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'uuid': ('api.fields.UuidField', [], {'unique': 'True', 'max_length': '32', 'primary_key': 'True'})
},
u'api.push': {
'Meta': {'ordering': "[u'-created']", 'unique_together': "((u'app', u'uuid'),)", 'object_name': 'Push'},
'app': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api.App']"}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'fingerprint': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
'receive_repo': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'receive_user': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'sha': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
'ssh_connection': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'ssh_original_command': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'uuid': ('api.fields.UuidField', [], {'unique': 'True', 'max_length': '32', 'primary_key': 'True'})
},
u'api.release': {
'Meta': {'ordering': "[u'-created']", 'unique_together': "((u'app', u'version'),)", 'object_name': 'Release'},
'app': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api.App']"}),
'build': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api.Build']"}),
'config': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api.Config']"}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'image': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
'summary': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'uuid': ('api.fields.UuidField', [], {'unique': 'True', 'max_length': '32', 'primary_key': 'True'}),
'version': ('django.db.models.fields.PositiveIntegerField', [], {})
},
u'auth.group': {
'Meta': {'object_name': 'Group'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
u'auth.permission': {
'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
u'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
u'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
}
}

complete_apps = ['api']
12 changes: 11 additions & 1 deletion controller/api/models.py
Expand Up @@ -127,8 +127,18 @@ def delete(self, *args, **kwargs):
c.destroy()
return super(App, self).delete(*args, **kwargs)

def deploy(self, release):
def deploy(self, release, initial=False):
tasks.deploy_release.delay(self, release).get()
# TODO: figure out if the logic below is what we really want
if initial:
# if there is procfile with a web worker, scale by web=1
if release.build.procfile and 'web' in release.build.procfile:
self.structure = {'web': 1}
# otherwise assume dockerfile, scale cmd=1
else:
release.build.app.structure = {'cmd': 1}
self.save()
self.scale()

def destroy(self, *args, **kwargs):
return self.delete(*args, **kwargs)
Expand Down
45 changes: 40 additions & 5 deletions controller/api/tests/test_build.py
Expand Up @@ -7,7 +7,6 @@
from __future__ import unicode_literals

import json
import unittest

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

def test_build_default_containers(self):
url = '/api/apps'
body = {'cluster': 'autotest'}
response = self.client.post(url, json.dumps(body), content_type='application/json')
self.assertEqual(response.status_code, 201)
app_id = response.data['id']
# post a new build
url = "/api/apps/{app_id}/builds".format(**locals())
body = {'image': 'autotest/example'}
response = self.client.post(url, json.dumps(body), content_type='application/json')
self.assertEqual(response.status_code, 201)
# test default container
url = "/api/apps/{app_id}/containers/cmd".format(**locals())
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data['results']), 1)
container = response.data['results'][0]
self.assertEqual(container['type'], 'cmd')
self.assertEqual(container['num'], 1)
# start with a new app
url = '/api/apps'
body = {'cluster': 'autotest'}
response = self.client.post(url, json.dumps(body), content_type='application/json')
self.assertEqual(response.status_code, 201)
app_id = response.data['id']
# post a new build with procfile
url = "/api/apps/{app_id}/builds".format(**locals())
body = {'image': 'autotest/example', 'procfile': json.dumps({'web': 'node server.js',
'worker': 'node worker.js'})}
response = self.client.post(url, json.dumps(body), content_type='application/json')
self.assertEqual(response.status_code, 201)
# test listing/retrieving container info
url = "/api/apps/{app_id}/containers/web".format(**locals())
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data['results']), 1)
container = response.data['results'][0]
self.assertEqual(container['type'], 'web')
self.assertEqual(container['num'], 1)

def test_build_str(self):
"""Test the text representation of a build."""
url = '/api/apps'
Expand Down
16 changes: 16 additions & 0 deletions controller/api/tests/test_hooks.py
Expand Up @@ -156,6 +156,14 @@ def test_build_hook_procfile(self):
build = response.data['results'][0]
self.assertEqual(build['sha'], SHA)
self.assertEqual(build['procfile'], json.dumps(PROCFILE))
# test listing/retrieving container info
url = "/api/apps/{app_id}/containers/web".format(**locals())
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data['results']), 1)
container = response.data['results'][0]
self.assertEqual(container['type'], 'web')
self.assertEqual(container['num'], 1)

def test_build_hook_dockerfile(self):
"""Test creating a Dockerfile build via an API Hook"""
Expand Down Expand Up @@ -197,6 +205,14 @@ def test_build_hook_dockerfile(self):
build = response.data['results'][0]
self.assertEqual(build['sha'], SHA)
self.assertEqual(build['dockerfile'], DOCKERFILE)
# test default container
url = "/api/apps/{app_id}/containers/cmd".format(**locals())
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data['results']), 1)
container = response.data['results'][0]
self.assertEqual(container['type'], 'cmd')
self.assertEqual(container['num'], 1)

def test_config_hook(self):
"""Test reading Config via an API Hook"""
Expand Down
16 changes: 4 additions & 12 deletions controller/api/views.py
Expand Up @@ -344,17 +344,8 @@ def post_save(self, build, created=False):
if created:
release = build.app.release_set.latest()
self.release = release.new(self.request.user, build=build)
build.app.deploy(self.release)
# if the structure is empty (first build)
if build.app.structure == {}:
# if there is procfile with a web worker, scale by web=1
if 'web' in build.procfile:
build.app.structure = {'web': 1}
# otherwise assume dockerfile, scale cmd=1
else:
build.app.structure = {'cmd': 1}
build.app.save()
build.app.scale()
initial = True if build.app.structure == {} else False
build.app.deploy(self.release, initial=initial)

def get_success_headers(self, data):
headers = super(AppBuildViewSet, self).get_success_headers(data)
Expand Down Expand Up @@ -521,7 +512,8 @@ def post_save(self, build, created=False):
if created:
release = build.app.release_set.latest()
new_release = release.new(build.owner, build=build)
build.app.deploy(new_release)
initial = True if build.app.structure == {} else False
build.app.deploy(new_release, initial=initial)


class ConfigHookViewSet(BaseHookViewSet):
Expand Down

0 comments on commit cdbd9b9

Please sign in to comment.