Skip to content

Commit cae586a

Browse files
committed
feat(release): add deploy release
1 parent 2c26292 commit cae586a

7 files changed

Lines changed: 99 additions & 45 deletions

File tree

rootfs/api/models/app.py

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -220,30 +220,35 @@ def scale(self, user, structure):
220220
app_settings = self.appsettings_set.latest()
221221
self._scale(user, structure, release, app_settings)
222222

223-
def pipeline(self, release, force_deploy=False, rollback_on_failure=True):
223+
def prepare(self, release):
224+
prefix = f"[pipeline] prepare {release.version_name}"
225+
if release.build.dryccfile:
226+
if 'run' in release.build.dryccfile and release.get_run_trigger():
227+
self.log(f"{prefix} starts running pipeline.run")
228+
job_name = self.run(
229+
self.owner, release.get_run_image(), command=release.get_run_command(),
230+
args=release.get_run_args(), timeout=release.get_run_timeout(),
231+
expires=release.get_run_timeout())
232+
state, labels = 'initializing', {'job-name': job_name}
233+
for count, state in enumerate(self.scheduler().pod.watch(
234+
self.id, labels, settings.DRYCC_PILELINE_RUN_TIMEOUT)):
235+
self.log(f"{prefix} waiting for pipeline.run: {state} * {count}")
236+
if state != 'down':
237+
raise DryccException(f'pipeline run state error: {state}')
238+
return True
239+
return False
240+
241+
def pipeline(self, release, force_deploy=False):
224242
prefix = f"[pipeline] release {release.version_name}"
225243
try:
226244
self.log(f"{prefix} starts running...")
227245
procfile_types = release.diff_procfile_types()
228-
if release.build.dryccfile:
229-
if 'run' in release.build.dryccfile and release.get_run_trigger():
230-
self.log(f"{prefix} starts running pipeline.run")
231-
job_name = self.run(
232-
self.owner, release.get_run_image(), command=release.get_run_command(),
233-
args=release.get_run_args(), timeout=release.get_run_timeout(),
234-
expires=release.get_run_timeout())
235-
state, labels = 'initializing', {'job-name': job_name}
236-
for count, state in enumerate(self.scheduler().pod.watch(
237-
self.id, labels, settings.DRYCC_PILELINE_RUN_TIMEOUT)):
238-
self.log(f"{prefix} waiting for pipeline.run: {state} * {count}")
239-
if state != 'down':
240-
raise DryccException(f'pipeline run state error: {state}')
246+
self.prepare(release)
241247
if procfile_types is None or len(procfile_types) > 0:
242248
self.log(f"{prefix} starts running pipeline.deploy")
243-
app_settings = self.appsettings_set.latest()
244-
if app_settings.autorollback is False:
249+
rollback_on_failure = self.appsettings_set.latest().autorollback
250+
if not rollback_on_failure:
245251
self.log(f"{prefix} deploy do not rollback on failure")
246-
rollback_on_failure = False
247252
self.deploy(release, procfile_types, force_deploy, rollback_on_failure)
248253
else:
249254
self.log(f"{prefix} no changes, skip executing pipeline.deploy")
@@ -617,7 +622,7 @@ def state_to_k8s(self):
617622
if len(procfile_types) == 0:
618623
self.log('the cluster status is the latest, skipping deployment...')
619624
return
620-
self.deploy(release, procfile_types)
625+
self.deploy(release, procfile_types, False, False)
621626

622627
def set_application_config(self, release, procfile_type):
623628
"""
@@ -750,8 +755,7 @@ def _deploy(self, deploys, procfile_types, prev_release,
750755
# This goes in the log before the rollback starts
751756
self.log(err, logging.ERROR)
752757
# revert all process types to old release
753-
self.deploy(prev_release, procfile_types,
754-
force_deploy=True, rollback_on_failure=False)
758+
self.deploy(prev_release, procfile_types, True, False)
755759
# let it bubble up
756760
raise DryccException('{}\n{}'.format(err, str(e))) from e
757761

rootfs/api/models/build.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ def create_release(self, user, *args, **kwargs):
7979
build=self,
8080
config=latest_release.config,
8181
)
82-
run_pipeline.delay(new_release)
82+
run_pipeline.delay(new_release, force_deploy=False)
8383
return new_release
8484
except Exception as e:
8585
# check if the exception is during create or publish

rootfs/api/models/release.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ class Meta:
4747
def __str__(self):
4848
return "{0}-{1}".format(self.app.id, self.version_name)
4949

50+
@property
51+
def deploying(self):
52+
return self.build is not None and self.state == "created"
53+
5054
@property
5155
def procfile_types(self):
5256
if self.build is not None:
@@ -222,19 +226,16 @@ def rollback(self, user, version=None):
222226
raise DryccException('version cannot be below 0')
223227
elif version == 1:
224228
raise DryccException('Cannot roll back to initial release.')
225-
226229
prev = self.app.release_set.get(version=version)
227230
if prev.failed:
228231
raise DryccException('Cannot roll back to failed release.')
229-
app_settings = self.app.appsettings_set.latest()
230232
latest_version = self.app.release_set.latest().version
231233
new_release = self.new(
232234
user,
233235
build=prev.build,
234236
config=prev.config,
235237
summary="{} rolled back to v{}".format(user, version),
236238
)
237-
238239
if self.build is not None:
239240
run_pipeline.delay(new_release, force_deploy=True)
240241
return new_release

rootfs/api/tasks.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -66,19 +66,12 @@ def run_pipeline(release, *args, **kwargs):
6666
autoretry_for=(ServiceUnavailable, ),
6767
retry_kwargs={'max_retries': None}
6868
)
69-
def run_deploy(release, config):
69+
def run_deploy(release, *args, **kwargs):
7070
task_id = uuid.uuid4().hex
7171
signals.request_started.send(sender=task_id)
7272
try:
7373
if release.build is not None:
74-
procfile_types = set()
75-
for field, diff in config.diff().items():
76-
if field in config.procfile_fields:
77-
for value in diff.values():
78-
procfile_types.update(value.keys())
79-
# all_diff_fields changed, deploy all.
80-
procfile_types = procfile_types if procfile_types else None
81-
config.app.deploy(release, procfile_types)
74+
release.app.deploy(release, *args, **kwargs)
8275
release.state = "succeed"
8376
release.save()
8477
except Exception as e:

rootfs/api/tests/test_release.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,35 @@ def test_response_data(self, mock_requests):
199199
}
200200
self.assertEqual(response.data, response.data | expected)
201201

202+
def test_release_deploy(self, mock_requests):
203+
app_id = self.create_app()
204+
# try to rollback with only 1 release extant, expecting 400
205+
url = f"/v2/apps/{app_id}/releases/deploy/"
206+
# app.deploy
207+
with mock.patch('api.models.app.App.deploy') as mock_deploy:
208+
mock_deploy.return_value = None
209+
response = self.client.post(url, {"types": "web"})
210+
self.assertEqual(response.status_code, 400)
211+
212+
# post a new build
213+
build_url = f"/v2/apps/{app_id}/builds"
214+
body = {
215+
'image': 'autotest/example',
216+
'stack': 'heroku-18',
217+
'sha': 'a'*40,
218+
'procfile': {
219+
'web': 'node server.js',
220+
'worker': 'node worker.js'
221+
}
222+
}
223+
response = self.client.post(build_url, body)
224+
self.assertEqual(response.status_code, 201)
225+
# app.deploy
226+
with mock.patch('api.models.app.App.deploy') as mock_deploy:
227+
mock_deploy.return_value = None
228+
response = self.client.post(url, {"types": "web"})
229+
self.assertEqual(response.status_code, 204)
230+
202231
def test_release_rollback(self, mock_requests):
203232
app_id = self.create_app()
204233
app = App.objects.get(id=app_id)
@@ -887,7 +916,7 @@ def test_diff_procfile_types(self, mock_requests):
887916
with mock.patch('scheduler.resources.pod.Pod.watch') as mock_kube:
888917
with mock.patch('api.models.app.logger') as mock_logger:
889918
mock_kube.return_value = ['up', 'down']
890-
app.pipeline(release, False, True)
919+
app.pipeline(release, False)
891920
self.assertEqual(release.state, "succeed")
892921
prefix = f"[{release.app.id}]: [pipeline] release v{release.version}"
893922
exp_msg = f"{prefix} no changes, skip executing pipeline.deploy"

rootfs/api/urls.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@
3939
re_path(
4040
r"^apps/(?P<id>{})/releases/v(?P<version>[0-9]+)/?$".format(settings.APP_URL_REGEX),
4141
views.ReleaseViewSet.as_view({'get': 'retrieve'})),
42+
re_path(
43+
r"^apps/(?P<id>{})/releases/deploy/?$".format(settings.APP_URL_REGEX),
44+
views.ReleaseViewSet.as_view({'post': 'deploy'})),
4245
re_path(
4346
r"^apps/(?P<id>{})/releases/rollback/?$".format(settings.APP_URL_REGEX),
4447
views.ReleaseViewSet.as_view({'post': 'rollback'})),

rootfs/api/views.py

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -387,13 +387,21 @@ class ConfigViewSet(ReleasableViewSet):
387387

388388
def post_save(self, config):
389389
latest_release = config.app.release_set.filter(failed=False).latest()
390-
if latest_release.build is not None and latest_release.state == "created":
390+
if latest_release.deploying:
391391
config.delete()
392392
raise DryccException('There is an executing pipeline, please wait')
393393
try:
394394
release = latest_release.new(
395395
self.request.user, config=config, build=latest_release.build)
396-
run_deploy.delay(release, config)
396+
procfile_types = set()
397+
for field, diff in config.diff().items():
398+
if field in config.procfile_fields:
399+
for value in diff.values():
400+
procfile_types.update(value.keys())
401+
# all_diff_fields changed, deploy all.
402+
procfile_types = procfile_types if procfile_types else None
403+
rollback_on_failure = config.app.appsettings_set.latest().autorollback
404+
run_deploy.delay(release, procfile_types, False, rollback_on_failure)
397405
except Exception as e:
398406
config.delete()
399407
if isinstance(e, AlreadyExists):
@@ -451,15 +459,11 @@ def describe(self, *args, **kwargs):
451459

452460
def restart(self, request, *args, **kwargs):
453461
app = self.get_app()
454-
ptypes = []
455-
types = request.data.get("types", "").split(",")
456-
types = [ptype for ptype in set(types) if ptype != ""]
457-
if not types:
458-
# all ptypes need to restart
459-
ptypes = app.structure.keys()
462+
ptypes = set([ptype for ptype in request.data.get("types", "").split(",") if ptype])
463+
if not ptypes:
464+
ptypes = app.structure.keys() # all ptypes need to restart
460465
else:
461-
ptypes = [ptype for ptype in types if ptype in app.structure]
462-
invalid_ptypes = set(types) - set(ptypes)
466+
invalid_ptypes = ptypes.difference(app.structure)
463467
if len(invalid_ptypes) != 0:
464468
raise DryccException("process type {} is not included in procfile".
465469
format(','.join(invalid_ptypes)))
@@ -470,7 +474,7 @@ def restart(self, request, *args, **kwargs):
470474
def scale(self, request, **kwargs):
471475
app = self.get_app()
472476
latest_release = app.release_set.filter(failed=False).latest()
473-
if latest_release.build is not None and latest_release.state == "created":
477+
if latest_release.deploying:
474478
raise DryccException('There is an executing pipeline, please wait')
475479
scale_app.delay(app, request.user, request.data)
476480
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -629,12 +633,32 @@ def get_object(self, **kwargs):
629633
qs = self.get_queryset(**kwargs)
630634
return get_object_or_404(qs, version=self.kwargs['version'])
631635

636+
def deploy(self, request, **kwargs):
637+
"""Deploy the latest release"""
638+
release = self.get_app().release_set.latest()
639+
if release.deploying:
640+
raise DryccException('There is an executing pipeline, please wait')
641+
procfile_types = set(
642+
[ptype for ptype in request.data.get("types", "").split(",") if ptype])
643+
if not procfile_types:
644+
procfile_types = release.app.structure.keys() # all procfile_types need to deploy
645+
else:
646+
invalid_procfile_types = procfile_types.difference(release.app.structure)
647+
if len(invalid_procfile_types) != 0:
648+
raise DryccException("process type {} is not included in procfile".
649+
format(','.join(invalid_procfile_types)))
650+
rollback_on_failure = release.app.appsettings_set.latest().autorollback
651+
run_deploy.delay(release, procfile_types, False, rollback_on_failure)
652+
return Response(status=status.HTTP_204_NO_CONTENT)
653+
632654
def rollback(self, request, **kwargs):
633655
"""
634656
Create a new release as a copy of the state of the compiled slug and config vars of a
635657
previous release.
636658
"""
637659
latest_release = self.get_app().release_set.filter(failed=False).latest()
660+
if latest_release.deploying:
661+
raise DryccException('There is an executing pipeline, please wait')
638662
new_release = latest_release.rollback(request.user, request.data.get('version', None))
639663
response = {'version': new_release.version}
640664
return Response(response, status=status.HTTP_201_CREATED)

0 commit comments

Comments
 (0)