Skip to content

Commit 8f943d9

Browse files
committed
fix(version):Use a different version number for each release
1 parent 12b5bfd commit 8f943d9

10 files changed

Lines changed: 166 additions & 40 deletions

File tree

rootfs/api/management/commands/load_db_state_to_k8s.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def handle(self, *args, **options):
2424
# deploy applications
2525
print("Deploying available applications")
2626
for application in App.objects.all():
27-
rel = application.release_set.latest()
27+
rel = application.release_set.filter(failed=False).latest()
2828
if rel.build is None:
2929
print('WARNING: {} has no build associated with '
3030
'its latest release. Skipping deployment...'.format(application))
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# -*- coding: utf-8 -*-
2+
# Generated by Django 1.10.1 on 2016-10-03 18:50
3+
from __future__ import unicode_literals
4+
5+
from django.db import migrations, models
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
('api', '0019_auto_20160930_2351'),
12+
]
13+
14+
operations = [
15+
migrations.AddField(
16+
model_name='release',
17+
name='failed',
18+
field=models.BooleanField(default=False),
19+
),
20+
]

rootfs/api/models/app.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ def _get_command(self, container_type):
138138
"""
139139
try:
140140
# FIXME: remove slugrunner's hardcoded entrypoint
141-
release = self.release_set.latest()
141+
release = self.release_set.filter(failed=False).latest()
142142
if release.build.dockerfile or not release.build.sha:
143143
return [release.build.procfile[container_type]]
144144

@@ -161,7 +161,7 @@ def _get_entrypoint(self, container_type):
161161

162162
# if this is a procfile-based app, switch the entrypoint to slugrunner's default
163163
# FIXME: remove slugrunner's hardcoded entrypoint
164-
release = self.release_set.latest()
164+
release = self.release_set.filter(failed=False).latest()
165165
if release.build.procfile and \
166166
release.build.sha and not \
167167
release.build.dockerfile:
@@ -352,10 +352,10 @@ def scale(self, user, structure): # noqa
352352
# use create to make sure minimum resources are created
353353
self.create()
354354

355-
if self.release_set.latest().build is None:
355+
if self.release_set.filter(failed=False).latest().build is None:
356356
raise DeisException('No build associated with this release')
357357

358-
release = self.release_set.latest()
358+
release = self.release_set.filter(failed=False).latest()
359359

360360
# Validate structure
361361
try:
@@ -401,7 +401,7 @@ def scale(self, user, structure): # noqa
401401
return False
402402

403403
def _scale_pods(self, scale_types):
404-
release = self.release_set.latest()
404+
release = self.release_set.filter(failed=False).latest()
405405
app_settings = self.appsettings_set.latest()
406406

407407
# use slugrunner image for app if buildpack app otherwise use normal image
@@ -577,7 +577,7 @@ def verify_application_health(self, **kwargs):
577577
only run after kubernetes has reported all pods as healthy
578578
"""
579579
# Bail out early if the application is not routable
580-
release = self.release_set.latest()
580+
release = self.release_set.filter(failed=False).latest()
581581
app_settings = self.appsettings_set.latest()
582582
if not kwargs.get('routable', False) and app_settings.routable:
583583
return
@@ -681,7 +681,7 @@ def pod_name(size=5, chars=string.ascii_lowercase + string.digits):
681681
return ''.join(random.choice(chars) for _ in range(size))
682682

683683
"""Run a one-off command in an ephemeral app container."""
684-
release = self.release_set.latest()
684+
release = self.release_set.filter(failed=False).latest()
685685
if release.build is None:
686686
raise DeisException('No build associated with this release to run this command')
687687

@@ -769,7 +769,7 @@ def _scheduler_filter(self, **kwargs):
769769

770770
# always supply a version, either latest or a specific one
771771
if 'release' not in kwargs or kwargs['release'] is None:
772-
release = self.release_set.latest()
772+
release = self.release_set.filter(failed=False).latest()
773773
else:
774774
release = self.release_set.get(version=kwargs['release'])
775775

rootfs/api/models/build.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@ def version(self):
5252
return 'git-{}'.format(self.sha) if self.source_based else 'latest'
5353

5454
def create(self, user, *args, **kwargs):
55-
latest_release = self.app.release_set.latest()
56-
55+
latest_release = self.app.release_set.filter(failed=False).latest()
56+
latest_version = self.app.release_set.latest().version
5757
try:
5858
new_release = latest_release.new(
5959
user,
@@ -64,24 +64,29 @@ def create(self, user, *args, **kwargs):
6464
self.app.deploy(new_release)
6565
return new_release
6666
except Exception as e:
67+
# check if the exception is during create or publish
68+
if ('new_release' not in locals() and
69+
self.app.release_set.latest().version == latest_version+1):
70+
new_release = self.app.release_set.latest()
6771
if 'new_release' in locals():
68-
new_release.delete()
69-
self.delete()
72+
new_release.failed = True
73+
new_release.summary = "{} deployed {} which failed".format(self.owner, str(self.uuid)[:7]) # noqa
74+
new_release.save()
75+
else:
76+
self.delete()
7077

7178
raise DeisException(str(e)) from e
7279

7380
def save(self, **kwargs):
74-
try:
75-
removed = {}
76-
previous_build = self.app.build_set.latest()
77-
for proc in previous_build.procfile:
81+
removed = {}
82+
previous_release = self.app.release_set.filter(failed=False).latest()
83+
if previous_release.build is not None:
84+
for proc in previous_release.build.procfile:
7885
if proc not in self.procfile:
7986
# Scale proc type down to 0
8087
removed[proc] = 0
8188

8289
self.app.scale(self.owner, removed)
83-
except Build.DoesNotExist:
84-
pass
8590

8691
return super(Build, self).save(**kwargs)
8792

rootfs/api/models/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ def save(self, **kwargs):
148148
try:
149149
# Get config from the latest available release
150150
try:
151-
previous_config = self.app.release_set.latest().config
151+
previous_config = self.app.release_set.filter(failed=False).latest().config
152152
except Release.DoesNotExist:
153153
# If that doesn't exist then fallback on app config
154154
# usually means a totally new app

rootfs/api/models/release.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class Release(UuidAuditedModel):
2323
app = models.ForeignKey('App', on_delete=models.CASCADE)
2424
version = models.PositiveIntegerField()
2525
summary = models.TextField(blank=True, null=True)
26+
failed = models.BooleanField(default=False)
2627

2728
config = models.ForeignKey('Config', on_delete=models.CASCADE)
2829
build = models.ForeignKey('Build', null=True, on_delete=models.CASCADE)
@@ -77,7 +78,7 @@ def new(self, user, config, build, summary=None, source_version='latest'):
7778
Releases start at v1 and auto-increment.
7879
"""
7980
# construct fully-qualified target image
80-
new_version = self.version + 1
81+
new_version = self.app.release_set.latest().version + 1
8182
# create new release and auto-increment version
8283
release = Release.objects.create(
8384
owner=user, app=self.app, config=config,
@@ -202,7 +203,7 @@ def previous(self):
202203

203204
try:
204205
# Get the Release previous to this one
205-
prev_release = releases.latest()
206+
prev_release = releases.filter(failed=False).latest()
206207
except Release.DoesNotExist:
207208
prev_release = None
208209
return prev_release
@@ -218,6 +219,9 @@ def rollback(self, user, version=None):
218219
raise DeisException('Cannot roll back to initial release.')
219220

220221
prev = self.app.release_set.get(version=version)
222+
if prev.failed:
223+
raise DeisException('Cannot roll back to failed release.')
224+
latest_version = self.app.release_set.latest().version
221225
new_release = self.new(
222226
user,
223227
build=prev.build,
@@ -230,8 +234,14 @@ def rollback(self, user, version=None):
230234
self.app.deploy(new_release, force_deploy=True)
231235
return new_release
232236
except Exception as e:
237+
# check if the exception is during create or publish
238+
if ('new_release' not in locals() and 'latest_version' in locals() and
239+
self.app.release_set.latest().version == latest_version+1):
240+
new_release = self.app.release_set.latest()
233241
if 'new_release' in locals():
234-
new_release.delete()
242+
new_release.failed = True
243+
new_release.summary = "{} performed roll back to a release that failed".format(self.owner) # noqa
244+
new_release.save()
235245
raise DeisException(str(e)) from e
236246

237247
def cleanup_old(self): # noqa

rootfs/api/tests/test_build.py

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from unittest import mock
1212
from rest_framework.authtoken.models import Token
1313

14-
from api.models import Build
14+
from api.models import Build, App
1515
from registry.dockerclient import RegistryException
1616
from scheduler import KubeException
1717

@@ -401,7 +401,7 @@ def test_build_image_in_registry_with_auth_no_port(self, mock_requests):
401401

402402
def test_release_create_failure(self, mock_requests):
403403
"""
404-
Cause an Exception in app.deploy to cause a release.delete in build.create
404+
Cause an Exception in app.deploy to cause a failed release in build.create
405405
"""
406406
app_id = self.create_app()
407407

@@ -422,7 +422,7 @@ def test_release_create_failure(self, mock_requests):
422422

423423
def test_release_registry_create_failure(self, mock_requests):
424424
"""
425-
Cause a RegistryException in app.deploy to cause a release.delete in build.create
425+
Cause a RegistryException in app.deploy to cause a failed release in build.create
426426
"""
427427
app_id = self.create_app()
428428

@@ -454,3 +454,36 @@ def test_build_deploy_kube_failure(self, mock_requests):
454454
body = {'image': 'autotest/example'}
455455
response = self.client.post(url, body)
456456
self.assertEqual(response.status_code, 400, response.data)
457+
458+
def test_build_failures(self, mock_requests):
459+
app_id = self.create_app()
460+
app = App.objects.get(id=app_id)
461+
462+
# deploy app to get a build
463+
url = "/v2/apps/{app_id}/builds".format(**locals())
464+
body = {'image': 'autotest/example'}
465+
response = self.client.post(url, body)
466+
self.assertEqual(response.status_code, 201, response.data)
467+
self.assertEqual(response.data['image'], body['image'])
468+
success_build = app.release_set.latest().build
469+
470+
# create a failed build to check that failed release is created
471+
with mock.patch('api.models.App.deploy') as mock_deploy:
472+
mock_deploy.side_effect = Exception('Boom!')
473+
474+
url = "/v2/apps/{app_id}/builds".format(**locals())
475+
body = {'image': 'autotest/example'}
476+
response = self.client.post(url, body)
477+
self.assertEqual(response.status_code, 400, response.data)
478+
self.assertEqual(app.release_set.latest().version, 3)
479+
self.assertEqual(app.release_set.filter(failed=False).latest().version, 2)
480+
481+
# create a config to see that the new release is created with the last successful build
482+
url = "/v2/apps/{app_id}/config".format(**locals())
483+
484+
body = {'values': json.dumps({'Test': 'test'})}
485+
response = self.client.post(url, body)
486+
self.assertEqual(response.status_code, 201, response.data)
487+
self.assertEqual(app.release_set.latest().version, 4)
488+
self.assertEqual(app.release_set.latest().build, success_build)
489+
self.assertEqual(app.build_set.count(), 2)

rootfs/api/tests/test_config.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,3 +331,41 @@ def test_config_app_not_exists(self, mock_requests):
331331
response = self.client.get(url)
332332
self.assertEqual(response.status_code, 404)
333333
self.assertEqual(response.data, 'No App matches the given query.')
334+
335+
def test_config_failures(self, mock_requests):
336+
app_id = self.create_app()
337+
app = App.objects.get(id=app_id)
338+
339+
# deploy app to get a build
340+
url = "/v2/apps/{}/builds".format(app_id)
341+
body = {'image': 'autotest/example'}
342+
response = self.client.post(url, body)
343+
self.assertEqual(response.status_code, 201, response.data)
344+
self.assertEqual(response.data['image'], body['image'])
345+
346+
# set an initial config value
347+
url = "/v2/apps/{app_id}/config".format(**locals())
348+
body = {'values': json.dumps({'NEW_URL1': 'http://localhost:8080/'})}
349+
response = self.client.post(url, body)
350+
self.assertEqual(response.status_code, 201, response.data)
351+
self.assertIn('NEW_URL1', response.data['values'])
352+
success_config = app.release_set.latest().config
353+
354+
# create a failed config to check that failed release is created
355+
with mock.patch('api.models.App.deploy') as mock_deploy:
356+
mock_deploy.side_effect = Exception('Boom!')
357+
url = '/v2/apps/{app_id}/config'.format(**locals())
358+
body = {'values': json.dumps({'test': "testvalue"})}
359+
response = self.client.post(url, body)
360+
self.assertEqual(response.status_code, 400)
361+
self.assertEqual(app.release_set.latest().version, 4)
362+
self.assertEqual(app.release_set.filter(failed=False).latest().version, 3)
363+
364+
# create a build to see that the new release is created with the last successful config
365+
url = "/v2/apps/{}/builds".format(app_id)
366+
body = {'image': 'autotest/example'}
367+
response = self.client.post(url, body)
368+
self.assertEqual(response.status_code, 201, response.data)
369+
self.assertEqual(app.release_set.latest().version, 5)
370+
self.assertEqual(app.release_set.latest().config, success_config)
371+
self.assertEqual(app.config_set.count(), 3)

rootfs/api/tests/test_release.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ def test_response_data(self, mock_requests):
117117
response = self.client.get(url)
118118
for key in response.data.keys():
119119
self.assertIn(key, ['uuid', 'owner', 'created', 'updated', 'app', 'build', 'config',
120-
'summary', 'version'])
120+
'summary', 'version', 'failed'])
121121
expected = {
122122
'owner': self.user.username,
123123
'app': app_id,
@@ -263,6 +263,7 @@ def test_release_rollback_failure(self, mock_requests):
263263
Cause an Exception in app.deploy to cause a release.delete
264264
"""
265265
app_id = self.create_app()
266+
app = App.objects.get(id=app_id)
266267

267268
# deploy app to get a build
268269
url = "/v2/apps/{}/builds".format(app_id)
@@ -277,19 +278,27 @@ def test_release_rollback_failure(self, mock_requests):
277278
response = self.client.post(url, body)
278279
self.assertEqual(response.status_code, 201, response.data)
279280

280-
# update config to roll a new release
281-
url = '/v2/apps/{}/config'.format(app_id)
282-
body = {'values': json.dumps({'NEW_URL2': 'http://localhost:8080/'})}
283-
response = self.client.post(url, body)
284-
self.assertEqual(response.status_code, 201, response.data)
285-
286281
# app.deploy exception
287282
with mock.patch('api.models.App.deploy') as mock_deploy:
288283
mock_deploy.side_effect = Exception('Boom!')
289284
url = "/v2/apps/{}/releases/rollback/".format(app_id)
290285
body = {'version': 2}
291286
response = self.client.post(url, body)
292287
self.assertEqual(response.status_code, 400, response.data)
288+
self.assertEqual(app.release_set.latest().version, 4)
289+
290+
# update config to roll a new release
291+
url = '/v2/apps/{}/config'.format(app_id)
292+
body = {'values': json.dumps({'NEW_URL2': 'http://localhost:8080/'})}
293+
response = self.client.post(url, body)
294+
self.assertEqual(response.status_code, 201, response.data)
295+
296+
# try to rollback to v4 and verify that the rollback failed
297+
# (v4 is a failed release)
298+
url = "/v2/apps/{app_id}/releases/rollback/".format(**locals())
299+
body = {'version': 4}
300+
response = self.client.post(url, body)
301+
self.assertContains(response, 'Cannot roll back to failed release.', status_code=400)
293302

294303
# app.deploy exception followed by a KubeHTTPException of 404
295304
with mock.patch('api.models.App.deploy') as mock_deploy:

0 commit comments

Comments
 (0)