Skip to content

Commit 161b673

Browse files
author
Matthew Fisher
committed
Merge pull request #2159 from bacongobbler/premature_deploy
fix(controller): deploy only when Build is present
2 parents 49bba74 + 0b9bd3b commit 161b673

9 files changed

Lines changed: 340 additions & 111 deletions

File tree

controller/api/models.py

Lines changed: 75 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -134,10 +134,9 @@ def log(self, message):
134134
f.write(msg.encode('utf-8'))
135135

136136
def create(self, *args, **kwargs):
137-
"""Create a new application with an initial release"""
137+
"""Create a new application with an initial config and release"""
138138
config = Config.objects.create(owner=self.owner, app=self)
139-
build = Build.objects.create(owner=self.owner, app=self, image=settings.DEFAULT_BUILD)
140-
Release.objects.create(version=1, owner=self.owner, app=self, config=config, build=build)
139+
Release.objects.create(version=1, owner=self.owner, app=self, config=config, build=None)
141140

142141
def delete(self, *args, **kwargs):
143142
"""Delete this application including all containers"""
@@ -154,6 +153,8 @@ def _clean_app_logs(self):
154153

155154
def scale(self, user, structure): # noqa
156155
"""Scale containers up or down to match requested structure."""
156+
if self.release_set.latest().build is None:
157+
raise EnvironmentError('No build associated with this release')
157158
requested_structure = structure.copy()
158159
release = self.release_set.latest()
159160
# test for available process types
@@ -310,12 +311,12 @@ def logs(self):
310311

311312
def run(self, user, command):
312313
"""Run a one-off command in an ephemeral app container."""
313-
314314
# FIXME: remove the need for SSH private keys by using
315315
# a scheduler that supports one-off admin tasks natively
316316
if not settings.SSH_PRIVATE_KEY:
317317
raise EnvironmentError('Support for admin commands is not configured')
318-
318+
if self.release_set.latest().build is None:
319+
raise EnvironmentError('No build associated with this release to run this command')
319320
# TODO: add support for interactive shell
320321
msg = "{} runs '{}'".format(user.username, command)
321322
log_event(self, msg)
@@ -327,7 +328,7 @@ def run(self, user, command):
327328
release=self.release_set.latest(),
328329
type='run',
329330
num=c_num)
330-
image = c.release.image + ':v' + str(c.release.version)
331+
image = c.release.image
331332

332333
# check for backwards compatibility
333334
def _has_hostname(image):
@@ -374,7 +375,7 @@ class Container(UuidAuditedModel):
374375
protected=True, propagate=False)
375376

376377
def short_name(self):
377-
return "{}.{}.{}".format(self.release.app.id, self.type, self.num)
378+
return "{}.{}.{}".format(self.app.id, self.type, self.num)
378379
short_name.short_description = 'Name'
379380

380381
def __str__(self):
@@ -418,7 +419,7 @@ def clone(self, release):
418419

419420
@transition(field=state, source=INITIALIZED, target=CREATED, on_error=ERROR)
420421
def create(self):
421-
image = self.release.image + ':v' + str(self.release.version)
422+
image = self.release.image
422423
kwargs = {'memory': self.release.config.memory,
423424
'cpu': self.release.config.cpu,
424425
'tags': self.release.config.tags}
@@ -466,7 +467,10 @@ def destroy(self):
466467

467468
def run(self, command):
468469
"""Run a one-off command"""
469-
image = self.release.image + ':v' + str(self.release.version)
470+
if self.release.build is None:
471+
raise EnvironmentError('No build associated with this release '
472+
'to run this command')
473+
image = self.release.image
470474
job_id = self._job_id
471475
entrypoint = '/bin/bash'
472476
if self.release.build.procfile:
@@ -528,6 +532,23 @@ class Meta:
528532
ordering = ['-created']
529533
unique_together = (('app', 'uuid'),)
530534

535+
def create(self, user, *args, **kwargs):
536+
latest_release = self.app.release_set.latest()
537+
source_version = 'latest'
538+
if self.sha:
539+
source_version = 'git-{}'.format(self.sha)
540+
new_release = latest_release.new(user,
541+
build=self,
542+
config=latest_release.config,
543+
source_version=source_version)
544+
initial = True if self.app.structure == {} else False
545+
try:
546+
self.app.deploy(user, new_release, initial=initial)
547+
return new_release
548+
except RuntimeError:
549+
new_release.delete()
550+
raise
551+
531552
def __str__(self):
532553
return "{0}-{1}".format(self.app.id, self.uuid[:7])
533554

@@ -569,9 +590,7 @@ class Release(UuidAuditedModel):
569590
summary = models.TextField(blank=True, null=True)
570591

571592
config = models.ForeignKey('Config')
572-
build = models.ForeignKey('Build')
573-
# NOTE: image contains combined build + config, ready to run
574-
image = models.CharField(max_length=256, default=settings.DEFAULT_BUILD)
593+
build = models.ForeignKey('Build', null=True)
575594

576595
class Meta:
577596
get_latest_by = 'created'
@@ -581,36 +600,43 @@ class Meta:
581600
def __str__(self):
582601
return "{0}-v{1}".format(self.app.id, self.version)
583602

584-
def new(self, user, config=None, build=None, summary=None, source_version='latest'):
603+
@property
604+
def image(self):
605+
return '{}:v{}'.format(self.app.id, str(self.version))
606+
607+
def new(self, user, config, build, summary=None, source_version='latest'):
585608
"""
586609
Create a new application release using the provided Build and Config
587610
on behalf of a user.
588611
589612
Releases start at v1 and auto-increment.
590613
"""
591-
if not config:
592-
config = self.config
593-
if not build:
594-
build = self.build
595-
# always create a release off the latest image
596-
source_tag = 'git-{}'.format(build.sha) if build.sha else source_version
597-
source_image = '{}:{}'.format(build.image, source_tag)
598614
# construct fully-qualified target image
599615
new_version = self.version + 1
600-
tag = 'v{}'.format(new_version)
601-
release_image = '{}:{}'.format(self.app.id, tag)
602-
target_image = '{}'.format(self.app.id)
603616
# create new release and auto-increment version
604617
release = Release.objects.create(
605618
owner=user, app=self.app, config=config,
606-
build=build, version=new_version, image=target_image, summary=summary)
619+
build=build, version=new_version, summary=summary)
620+
try:
621+
release.publish()
622+
except EnvironmentError as e:
623+
# If we cannot publish this app, just log and carry on
624+
logger.info(e)
625+
pass
626+
return release
627+
628+
def publish(self, source_version='latest'):
629+
if self.build is None:
630+
raise EnvironmentError('No build associated with this release to publish')
631+
source_tag = 'git-{}'.format(self.build.sha) if self.build.sha else source_version
632+
source_image = '{}:{}'.format(self.build.image, source_tag)
607633
# IOW, this image did not come from the builder
608634
# FIXME: remove check for mock registry module
609-
if not build.sha and 'mock' not in settings.REGISTRY_MODULE:
635+
if not self.build.sha and 'mock' not in settings.REGISTRY_MODULE:
610636
# we assume that the image is not present on our registry,
611637
# so shell out a task to pull in the repository
612638
data = {
613-
'src': build.image
639+
'src': self.build.image
614640
}
615641
requests.post(
616642
'{}/v1/repositories/{}/tags'.format(settings.REGISTRY_URL,
@@ -620,14 +646,12 @@ def new(self, user, config=None, build=None, summary=None, source_version='lates
620646
# update the source image to the repository we just imported
621647
source_image = self.app.id
622648
# if the image imported had a tag specified, use that tag as the source
623-
if ':' in build.image:
624-
if '/' not in build.image[build.image.rfind(':') + 1:]:
625-
source_image += build.image[build.image.rfind(':'):]
626-
649+
if ':' in self.build.image:
650+
if '/' not in self.build.image[self.build.image.rfind(':') + 1:]:
651+
source_image += self.build.image[self.build.image.rfind(':'):]
627652
publish_release(source_image,
628-
config.values,
629-
release_image,)
630-
return release
653+
self.config.values,
654+
self.image)
631655

632656
def previous(self):
633657
"""
@@ -645,6 +669,24 @@ def previous(self):
645669
prev_release = None
646670
return prev_release
647671

672+
def rollback(self, user, version):
673+
if version < 1:
674+
raise EnvironmentError('version cannot be below 0')
675+
summary = "{} rolled back to v{}".format(user, version)
676+
prev = self.app.release_set.get(version=version)
677+
new_release = self.new(
678+
user,
679+
build=prev.build,
680+
config=prev.config,
681+
summary=summary,
682+
source_version='v{}'.format(version))
683+
try:
684+
self.app.deploy(user, new_release)
685+
return new_release
686+
except RuntimeError:
687+
new_release.delete()
688+
raise
689+
648690
def save(self, *args, **kwargs): # noqa
649691
if not self.summary:
650692
self.summary = ''

0 commit comments

Comments
 (0)