@@ -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