Skip to content

Commit f7d887d

Browse files
committed
Added support for deis release:rollback, fixes #382.
1 parent f021b55 commit f7d887d

5 files changed

Lines changed: 115 additions & 12 deletions

File tree

api/models.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -776,11 +776,6 @@ class Meta:
776776
def __str__(self):
777777
return "{0}-v{1}".format(self.app.id, self.version)
778778

779-
def rollback(self):
780-
# create a rollback log entry
781-
# call run
782-
raise NotImplementedError
783-
784779

785780
@receiver(release_signal)
786781
def new_release(sender, **kwargs):
@@ -791,7 +786,7 @@ def new_release(sender, **kwargs):
791786
Releases start at v1 and auto-increment.
792787
"""
793788
user, app, = kwargs['user'], kwargs['app']
794-
last_release = Release.objects.filter(app=app).order_by('-created')[0]
789+
last_release = app.release_set.latest()
795790
config = kwargs.get('config', last_release.config)
796791
build = kwargs.get('build', last_release.build)
797792
# overwrite config with build.config if the keys don't exist

api/tests/test_release.py

Lines changed: 67 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
from __future__ import unicode_literals
88

99
import json
10-
import unittest
1110
import uuid
1211

1312
from django.test import TestCase
@@ -136,19 +135,81 @@ def test_release(self):
136135
self.assertEqual(self.client.delete(url).status_code, 405)
137136
return release3
138137

139-
@unittest.expectedFailure
140138
def test_release_rollback(self):
141139
url = '/api/apps'
142140
body = {'formation': 'autotest'}
143141
response = self.client.post(url, json.dumps(body), content_type='application/json')
144142
self.assertEqual(response.status_code, 201)
145143
app_id = response.data['id']
146-
# check to see that an initial release was created
144+
# try to rollback with only 1 release extant, expecting 404
145+
url = "/api/apps/{app_id}/releases/rollback/".format(**locals())
146+
response = self.client.post(url, content_type='application/json')
147+
self.assertEqual(response.status_code, 404)
148+
# update config to roll a new release
149+
url = '/api/apps/{app_id}/config'.format(**locals())
150+
body = {'values': json.dumps({'NEW_URL1': 'http://localhost:8080/'})}
151+
response = self.client.post(
152+
url, json.dumps(body), content_type='application/json')
153+
self.assertEqual(response.status_code, 201)
154+
# update the build to roll a new release
155+
url = '/api/apps/{app_id}/builds'.format(**locals())
156+
build_config = json.dumps({'PATH': 'bin:/usr/local/bin:/usr/bin:/bin'})
157+
body = {
158+
'sha': uuid.uuid4().hex,
159+
'slug_size': 4096000,
160+
'procfile': json.dumps({'web': 'node server.js'}),
161+
'url':
162+
'http://deis.local/slugs/1c52739bbf3a44d3bfb9a58f7bbdd5fb.tar.gz',
163+
'checksum': uuid.uuid4().hex, 'config': build_config,
164+
}
165+
response = self.client.post(
166+
url, json.dumps(body), content_type='application/json')
167+
self.assertEqual(response.status_code, 201)
168+
# rollback and check to see that a 4th release was created
169+
# with the build and config of release #2
170+
url = "/api/apps/{app_id}/releases/rollback/".format(**locals())
171+
response = self.client.post(url, content_type='application/json')
172+
self.assertEqual(response.status_code, 201)
173+
url = '/api/apps/{app_id}/releases'.format(**locals())
174+
response = self.client.get(url, content_type='application/json')
175+
self.assertEqual(response.status_code, 200)
176+
self.assertEqual(response.data['count'], 4)
177+
url = '/api/apps/{app_id}/releases/2'.format(**locals())
178+
response = self.client.get(url, content_type='application/json')
179+
self.assertEqual(response.status_code, 200)
180+
release2 = response.data
181+
self.assertEquals(release2['version'], 2)
182+
url = '/api/apps/{app_id}/releases/4'.format(**locals())
183+
response = self.client.get(url, content_type='application/json')
184+
self.assertEqual(response.status_code, 200)
185+
release4 = response.data
186+
self.assertEquals(release4['version'], 4)
187+
self.assertNotEqual(release2['uuid'], release4['uuid'])
188+
self.assertEqual(release2['build'], release4['build'])
189+
self.assertEqual(release2['config'], release4['config'])
190+
# rollback explicitly to release #1 and check that a 5th release
191+
# was created with the build and config of release #1
192+
url = "/api/apps/{app_id}/releases/rollback/".format(**locals())
193+
body = {'version': 1}
194+
response = self.client.post(
195+
url, json.dumps(body), content_type='application/json')
196+
self.assertEqual(response.status_code, 201)
147197
url = '/api/apps/{app_id}/releases'.format(**locals())
198+
response = self.client.get(url, content_type='application/json')
199+
self.assertEqual(response.status_code, 200)
200+
self.assertEqual(response.data['count'], 5)
201+
url = '/api/apps/{app_id}/releases/1'.format(**locals())
148202
response = self.client.get(url)
149-
uuid = response.data['results'][0]['uuid']
150-
release = Release.objects.get(uuid=uuid)
151-
release.rollback() # raises NotImplementedError currently
203+
self.assertEqual(response.status_code, 200)
204+
release1 = response.data
205+
url = '/api/apps/{app_id}/releases/5'.format(**locals())
206+
response = self.client.get(url)
207+
self.assertEqual(response.status_code, 200)
208+
release5 = response.data
209+
self.assertEqual(release5['version'], 5)
210+
self.assertNotEqual(release1['uuid'], release5['uuid'])
211+
self.assertEqual(release1['build'], release5['build'])
212+
self.assertEqual(release1['config'], release5['config'])
152213

153214
def test_release_str(self):
154215
"""Test the text representation of a release."""

api/urls.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,10 @@
217217
218218
List all :class:`~api.models.Release`\s.
219219
220+
.. http:post:: /api/apps/(string:id)/releases/rollback/
221+
222+
Rollback to a previous :class:`~api.models.Release`.
223+
220224
221225
Application Infrastructure
222226
--------------------------
@@ -409,6 +413,8 @@
409413
views.AppBuildViewSet.as_view({'get': 'list', 'post': 'create'})),
410414
url(r'^apps/(?P<id>[-_\w]+)/releases/(?P<version>[0-9]+)/?',
411415
views.AppReleaseViewSet.as_view({'get': 'retrieve'})),
416+
url(r'^apps/(?P<id>[-_\w]+)/releases/rollback/?',
417+
views.AppReleaseViewSet.as_view({'post': 'rollback'})),
412418
url(r'^apps/(?P<id>[-_\w]+)/releases/?',
413419
views.AppReleaseViewSet.as_view({'get': 'list'})),
414420
# application infrastructure

api/views.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from Crypto.PublicKey import RSA
1010
from django.contrib.auth.models import AnonymousUser
1111
from django.contrib.auth.models import User
12+
from django.db import transaction
1213
from django.utils import timezone
1314
from guardian.shortcuts import assign_perm
1415
from guardian.shortcuts import get_objects_for_user
@@ -591,6 +592,24 @@ def get_object(self, *args, **kwargs):
591592
"""Get Release by version always."""
592593
return self.get_queryset(**kwargs).get(version=self.kwargs['version'])
593594

595+
def rollback(self, request, *args, **kwargs):
596+
"""
597+
Create a new release as a copy of the state of the compiled slug and
598+
config vars of a previous release.
599+
"""
600+
app = get_object_or_404(models.App, id=self.kwargs['id'])
601+
last_version = app.release_set.latest().version
602+
version = request.DATA.get('version', last_version - 1)
603+
if version < 1:
604+
return Response(status=status.HTTP_404_NOT_FOUND)
605+
prev = app.release_set.get(version=version)
606+
with transaction.atomic():
607+
app.release_set.create(owner=request.user, version=last_version + 1,
608+
build=prev.build, config=prev.config)
609+
app.converge()
610+
msg = "Rolled back to {}".format(version)
611+
return Response(msg, status=status.HTTP_201_CREATED)
612+
594613

595614
class AppContainerViewSet(OwnerViewSet):
596615
"""RESTful views for :class:`~api.models.Container`."""

client/deis.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1954,6 +1954,27 @@ def releases_list(self, args):
19541954
else:
19551955
raise ResponseError(response)
19561956

1957+
def releases_rollback(self, args):
1958+
"""
1959+
Roll back to a previous application release.
1960+
1961+
Usage: deis releases:rollback [--app=<app>] [<version>]
1962+
"""
1963+
app = args.get('--app')
1964+
if not app:
1965+
app = self._session.app
1966+
version = args.get('--version')
1967+
if version:
1968+
body = {'version': version}
1969+
else:
1970+
body = {}
1971+
url = "/api/apps/{app}/releases/rollback".format(**locals())
1972+
response = self._dispatch('post', url, json.dumps(body))
1973+
if response.status_code == requests.codes.created:
1974+
print(response.json())
1975+
else:
1976+
raise ResponseError(response)
1977+
19571978

19581979
def parse_args(cmd):
19591980
"""
@@ -1973,6 +1994,7 @@ def parse_args(cmd):
19731994
'ssh': 'nodes:ssh',
19741995
'open': 'apps:open',
19751996
'logs': 'apps:logs',
1997+
'rollback': 'releases:rollback',
19761998
'run': 'apps:run',
19771999
'sharing': 'perms:list',
19782000
'sharing:list': 'perms:list',

0 commit comments

Comments
 (0)