Skip to content

Commit c5aec20

Browse files
author
Gabriel Monroy
committed
feat(dockerfile): improve dockerfile and procfile workflow
- add SHA, Procfile and Dockerfile to Build model (if available) - fix operator precedence bug from deis/deis#890 - add container type check before scale operations (fixes #496) - add uniqueness constraint check on container type/num - use SHA in release summary if available - auto-scale web=1 for heroku workflow, cmd=1 for docker workflow - add tests for heroku and docker workflows, plus test for invalid process types
1 parent fa92f72 commit c5aec20

5 files changed

Lines changed: 365 additions & 9 deletions

File tree

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

controller/api/models.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,10 +133,18 @@ def deploy(self, release):
133133
def destroy(self, *args, **kwargs):
134134
return self.delete(*args, **kwargs)
135135

136-
def scale(self, **kwargs):
136+
def scale(self, **kwargs): # noqa
137137
"""Scale containers up or down to match requested."""
138138
requested_containers = self.structure.copy()
139139
release = self.release_set.latest()
140+
# test for available process types
141+
available_process_types = release.build.procfile or {}
142+
for container_type in requested_containers.keys():
143+
if container_type == 'cmd':
144+
continue # allow docker cmd types in case we don't have the image source
145+
if not container_type in available_process_types:
146+
raise EnvironmentError(
147+
'Container type {} does not exist in application'.format(container_type))
140148
msg = 'Containers scaled ' + ' '.join(
141149
"{}={}".format(k, v) for k, v in requested_containers.items())
142150
# iterate and scale by container type (web, worker, etc)
@@ -146,7 +154,7 @@ def scale(self, **kwargs):
146154
containers = list(self.container_set.filter(type=container_type).order_by('created'))
147155
# increment new container nums off the most recent container
148156
results = self.container_set.filter(type=container_type).aggregate(Max('num'))
149-
container_num = results.get('num__max') or 0 + 1
157+
container_num = (results.get('num__max') or 0) + 1
150158
requested = requested_containers.pop(container_type)
151159
diff = requested - len(containers)
152160
if diff == 0:
@@ -234,6 +242,7 @@ def __str__(self):
234242
class Meta:
235243
get_latest_by = '-created'
236244
ordering = ['created']
245+
unique_together = (('type', 'num'),)
237246

238247
def _get_job_id(self):
239248
app = self.app.id
@@ -352,6 +361,11 @@ class Build(UuidAuditedModel):
352361
app = models.ForeignKey('App')
353362
image = models.CharField(max_length=256)
354363

364+
# optional fields populated by builder
365+
sha = models.CharField(max_length=40, blank=True)
366+
procfile = JSONField(default='{}', blank=True)
367+
dockerfile = models.TextField(blank=True)
368+
355369
class Meta:
356370
get_latest_by = 'created'
357371
ordering = ['-created']
@@ -455,7 +469,10 @@ def save(self, *args, **kwargs):
455469
old_build = prev_release.build if prev_release else None
456470
# if the build changed, log it and who pushed it
457471
if self.build != old_build:
458-
self.summary += "{} deployed {}".format(self.build.owner, self.build.image)
472+
if self.build.sha:
473+
self.summary += "{} deployed {}".format(self.build.owner, self.build.sha[:7])
474+
else:
475+
self.summary += "{} deployed {}".format(self.build.owner, self.build.image)
459476
# compare this config to the previous config
460477
old_config = prev_release.config if prev_release else None
461478
# if the config data changed, log the dict diff

0 commit comments

Comments
 (0)