diff --git a/controller/api/migrations/0011_auto__del_unique_container_type_num.py b/controller/api/migrations/0011_auto__del_unique_container_type_num.py new file mode 100644 index 0000000000..e54ba0e1f3 --- /dev/null +++ b/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'] \ No newline at end of file diff --git a/controller/api/models.py b/controller/api/models.py index 58e14a8f0f..8333e65d2a 100644 --- a/controller/api/models.py +++ b/controller/api/models.py @@ -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) diff --git a/controller/api/tests/test_build.py b/controller/api/tests/test_build.py index 2f93eac984..a547a08fe1 100644 --- a/controller/api/tests/test_build.py +++ b/controller/api/tests/test_build.py @@ -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 @@ -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') @@ -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' diff --git a/controller/api/tests/test_hooks.py b/controller/api/tests/test_hooks.py index 16fd6204a1..bc5965c81c 100644 --- a/controller/api/tests/test_hooks.py +++ b/controller/api/tests/test_hooks.py @@ -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""" @@ -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""" diff --git a/controller/api/views.py b/controller/api/views.py index b5004a4614..8d8e41454c 100644 --- a/controller/api/views.py +++ b/controller/api/views.py @@ -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) @@ -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):