Skip to content

Commit f3ba668

Browse files
committed
chore(release): add autodeploy flag
1 parent fead61e commit f3ba668

12 files changed

Lines changed: 191 additions & 504 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Generated by Django 4.2.15 on 2024-08-27 02:18
2+
3+
from django.db import migrations, models
4+
5+
6+
def migration_dryccfile(apps, schema_editor):
7+
build_model = apps.get_model('api', 'Build')
8+
for build in build_model.objects.all():
9+
if build.dryccfile and isinstance(build.dryccfile.get('run'), dict):
10+
build.dryccfile['run'] = [build.dryccfile['run']]
11+
build.save()
12+
13+
14+
class Migration(migrations.Migration):
15+
16+
dependencies = [
17+
('api', '0011_merge_20240815_0955'),
18+
]
19+
20+
operations = [
21+
migrations.AddField(
22+
model_name='appsettings',
23+
name='autodeploy',
24+
field=models.BooleanField(default=True),
25+
),
26+
migrations.RunPython(migration_dryccfile),
27+
]

rootfs/api/models/app.py

Lines changed: 30 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,19 @@ def save(self, *args, **kwargs):
113113
def procfile_types(self):
114114
return list(self.structure.keys())
115115

116+
def check_procfile_types(self, procfile_types):
117+
"""
118+
check available procfile types
119+
"""
120+
if not procfile_types:
121+
procfile_types = self.procfile_types
122+
else:
123+
invalid_procfile_types = procfile_types.difference(self.procfile_types)
124+
if len(invalid_procfile_types) != 0:
125+
raise DryccException("process type {} is not included in procfile".
126+
format(','.join(invalid_procfile_types)))
127+
return procfile_types
128+
116129
def log(self, message, level=logging.INFO):
117130
"""Logs a message in the context of this application.
118131
@@ -220,38 +233,28 @@ def scale(self, user, structure):
220233
app_settings = self.appsettings_set.latest()
221234
self._scale(user, structure, release, app_settings)
222235

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):
236+
def pipeline(self, release, procfile_types=None, force_deploy=False):
242237
prefix = f"[pipeline] release {release.version_name}"
243238
try:
244-
self.log(f"{prefix} starts running...")
245-
procfile_types = release.diff_procfile_types()
246-
self.prepare(release)
247-
if procfile_types is None or len(procfile_types) > 0:
248-
self.log(f"{prefix} starts running pipeline.deploy")
239+
if release.build is not None:
240+
if release.build.dryccfile:
241+
for run in release.get_runners(procfile_types):
242+
self.log(f"{prefix} starts running pipeline.run {run['image']}")
243+
job_name = self.run(
244+
self.owner, run['image'], command=run['command'],
245+
args=run['args'], timeout=run['timeout'], expires=run['timeout']
246+
)
247+
state, labels = 'initializing', {'job-name': job_name}
248+
for count, state in enumerate(self.scheduler().pod.watch(
249+
self.id, labels, settings.DRYCC_PILELINE_RUN_TIMEOUT)):
250+
self.log(f"{prefix} waiting for pipeline.run: {state} * {count}")
251+
if state != 'down':
252+
raise DryccException(f'pipeline run state error: {state}')
253+
self.log(f"{prefix} starts running...")
249254
rollback_on_failure = self.appsettings_set.latest().autorollback
250255
if not rollback_on_failure:
251256
self.log(f"{prefix} deploy do not rollback on failure")
252257
self.deploy(release, procfile_types, force_deploy, rollback_on_failure)
253-
else:
254-
self.log(f"{prefix} no changes, skip executing pipeline.deploy")
255258
release.state = "succeed"
256259
except Exception as e:
257260
release.state = "crashed"
@@ -356,14 +359,7 @@ def pod_name(size=5, chars=string.ascii_lowercase + string.digits):
356359
try:
357360
# create application config and build the pod manifest
358361
self.set_application_config(release, PROCFILE_TYPE_RUN)
359-
self.scheduler().job.create(
360-
self.id,
361-
name,
362-
image if image else release.get_run_image(),
363-
command if command else release.get_run_command(),
364-
args if args else release.get_run_args(),
365-
**kwargs
366-
)
362+
self.scheduler().job.create(self.id, name, image, command, args, **kwargs)
367363
except Exception as e:
368364
err = '{} ({}): {}'.format(name, PROCFILE_TYPE_RUN, e)
369365
raise ServiceUnavailable(err) from e

rootfs/api/models/appsettings.py

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class AppSettings(UuidAuditedModel):
2020
owner = models.ForeignKey(User, on_delete=models.PROTECT)
2121
app = models.ForeignKey('App', on_delete=models.CASCADE)
2222
routable = models.BooleanField(default=True)
23+
autodeploy = models.BooleanField(default=True)
2324
autorollback = models.BooleanField(default=True)
2425
autoscale = models.JSONField(default=dict, blank=True)
2526
label = models.JSONField(default=dict, blank=True)
@@ -63,23 +64,14 @@ def previous(self):
6364
prev_app_settings = None
6465
return prev_app_settings
6566

66-
def _update_routable(self, previous_settings):
67-
old = getattr(previous_settings, 'routable', None)
68-
new = getattr(self, 'routable', None)
67+
def _update_field(self, field, previous_settings):
68+
old = getattr(previous_settings, field, None)
69+
new = getattr(self, field, None)
6970
# if nothing changed copy the settings from previous
7071
if new is None and old is not None:
71-
setattr(self, 'routable', old)
72+
setattr(self, field, old)
7273
elif old != new:
73-
self.summary += ["{} changed routablity from {} to {}".format(self.owner, old, new)]
74-
75-
def _update_autorollback(self, previous_settings):
76-
old = getattr(previous_settings, 'autorollback', None)
77-
new = getattr(self, 'autorollback', None)
78-
# if nothing changed copy the settings from previous
79-
if new is None and old is not None:
80-
setattr(self, 'autorollback', old)
81-
elif old != new:
82-
self.summary += ["{} changed autorollback from {} to {}".format(self.owner, old, new)]
74+
self.summary += ["{} changed {} from {} to {}".format(self.owner, field, old, new)]
8375

8476
def _update_autoscale(self, previous_settings):
8577
data = getattr(previous_settings, 'autoscale', {}).copy()
@@ -159,11 +151,15 @@ def _update_fields(self, ignore_update_fields=None):
159151
previous_settings = self.app.appsettings_set.latest()
160152
except AppSettings.DoesNotExist:
161153
pass
162-
update_fields = ["routable", "autorollback", "autoscale", "label"]
154+
update_fields = ["routable", "autodeploy", "autorollback", "autoscale", "label"]
163155
try:
164156
for update_field in update_fields:
165157
if ignore_update_fields is None or update_field not in ignore_update_fields:
166-
getattr(self, "_update_%s" % update_field)(previous_settings)
158+
method = getattr(self, "_update_%s" % update_field, None)
159+
if method:
160+
method(previous_settings)
161+
else:
162+
self._update_field(update_field, previous_settings)
167163
except (UnprocessableEntity, NotFound):
168164
raise
169165
except Exception as e:

rootfs/api/models/build.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,8 @@ def create_release(self, user, *args, **kwargs):
7979
build=self,
8080
config=latest_release.config,
8181
)
82-
run_pipeline.delay(new_release, force_deploy=False)
82+
if self.app.appsettings_set.latest().autorollback:
83+
run_pipeline.delay(new_release, force_deploy=False)
8384
return new_release
8485
except Exception as e:
8586
# check if the exception is during create or publish

rootfs/api/models/release.py

Lines changed: 18 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
from django.db import models
55
from django.db.models import Q
66
from django.contrib.auth import get_user_model
7-
from api.utils import dict_diff
87
from api.tasks import run_pipeline
98
from api.exceptions import DryccException, AlreadyExists
109
from scheduler import KubeHTTPException
@@ -61,43 +60,22 @@ def procfile_types(self):
6160
def version_name(self):
6261
return f'v{self.version}'
6362

64-
def get_run_image(self):
65-
"""
66-
In the run phase of dryccfile
67-
Return the kubernetes "container image" to be sent off to the scheduler.
68-
"""
69-
image = self.build.dryccfile.get('run', {}).get(
70-
'image', self.build.get_image('run'))
71-
return self.build.get_image(image, default_image=image)
72-
73-
def get_run_args(self):
74-
"""
75-
In the run phase of dryccfile
76-
Return the kubernetes "container arguments" to be sent off to the scheduler.
77-
"""
78-
return self.build.dryccfile.get('run', {}).get('args', [])
79-
80-
def get_run_command(self):
81-
"""
82-
In the run phase of dryccfile
83-
Return the kubernetes "container command" to be sent off to the scheduler.
84-
"""
85-
return self.build.dryccfile.get('run', {}).get('command', [])
86-
87-
def get_run_timeout(self):
88-
return int(self.build.dryccfile.get('run', {}).get(
89-
'timeout', settings.DRYCC_PILELINE_RUN_TIMEOUT))
90-
91-
def get_run_trigger(self):
92-
if 'ptypes' not in self.build.dryccfile.get('run', {}).get('when', {}):
93-
return True
94-
procfile_types = self.diff_procfile_types()
95-
if procfile_types is None:
96-
return True
97-
for procfile_type in procfile_types:
98-
if procfile_type in self.build.dryccfile['run']['when']['ptypes']:
99-
return True
100-
return False
63+
def get_runners(self, procfile_types):
64+
results = []
65+
procfile_types = self.procfile_types if not procfile_types else procfile_types
66+
for run in self.build.dryccfile.get('run', []):
67+
for container_type in procfile_types:
68+
when_ptypes = run.get('when', {}).get('ptypes', [])
69+
if not when_ptypes or container_type in when_ptypes:
70+
image = run.get('image', self.build.get_image(container_type))
71+
results.append({
72+
'image': self.build.get_image(image, default_image=image),
73+
'args': run.get('args', []),
74+
'command': run.get('command', []),
75+
'timeout': run.get('timeout', settings.DRYCC_PILELINE_RUN_TIMEOUT),
76+
})
77+
break
78+
return results
10179

10280
def get_deploy_image(self, container_type):
10381
"""
@@ -133,30 +111,6 @@ def get_deploy_command(self, container_type):
133111
return self.build.dryccfile.get(
134112
'deploy', {}).get(container_type, {}).get('command', [])
135113

136-
def diff_procfile_types(self):
137-
"""
138-
If returning None, it indicates that all procfile_types have changed.
139-
"""
140-
def _get_full_deploy(release):
141-
deploy = {}
142-
for procfile_type, value in release.build.dryccfile['deploy'].items():
143-
value['image'] = release.get_deploy_image(procfile_type)
144-
value['args'] = release.get_deploy_args(procfile_type)
145-
value['command'] = release.get_deploy_command(procfile_type)
146-
deploy[procfile_type] = value
147-
return deploy
148-
149-
pre_release = self.previous()
150-
if (pre_release and pre_release.build and
151-
pre_release.build.dryccfile and self.build and self.build.dryccfile):
152-
deploy = _get_full_deploy(self)
153-
pre_deploy = _get_full_deploy(pre_release)
154-
procfile_types = set()
155-
for value in dict_diff(deploy, pre_deploy).values():
156-
procfile_types = procfile_types.union(value.keys())
157-
return procfile_types
158-
return None
159-
160114
def log(self, message, level=logging.INFO):
161115
"""Logs a message in the context of this application.
162116
@@ -217,7 +171,7 @@ def previous(self):
217171
prev_release = None
218172
return prev_release
219173

220-
def rollback(self, user, version=None):
174+
def rollback(self, user, procfile_types=None, version=None):
221175
try:
222176
# if no version is provided then grab version from object
223177
version = (self.version - 1) if version is None else int(version)
@@ -237,7 +191,7 @@ def rollback(self, user, version=None):
237191
summary="{} rolled back to v{}".format(user, version),
238192
)
239193
if self.build is not None:
240-
run_pipeline.delay(new_release, force_deploy=True)
194+
run_pipeline.delay(new_release, procfile_types, force_deploy=True)
241195
return new_release
242196
except Exception as e:
243197
# check if the exception is during create or publish

rootfs/api/serializers/schemas/dryccfile.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,20 @@
1818
},
1919
},
2020
"run": {
21-
"type": "object",
22-
"properties": {
23-
"image": {"type": "string"},
24-
"command": {
25-
"type": "array",
26-
"items": {"type": "string"},
21+
"type": "array",
22+
"items": {
23+
"type": "object",
24+
"properties": {
25+
"image": {"type": "string"},
26+
"command": {
27+
"type": "array",
28+
"items": {"type": "string"},
29+
},
30+
"args": {
31+
"type": "array",
32+
"items": {"type": "string"},
33+
}
2734
},
28-
"args": {
29-
"type": "array",
30-
"items": {"type": "string"},
31-
}
3235
},
3336
},
3437
"deploy": {

rootfs/api/tasks.py

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -62,31 +62,6 @@ def run_pipeline(release, *args, **kwargs):
6262
signals.request_finished.send(sender=task_id)
6363

6464

65-
@shared_task(
66-
autoretry_for=(ServiceUnavailable, ),
67-
retry_kwargs={'max_retries': None}
68-
)
69-
def run_deploy(release, *args, **kwargs):
70-
task_id = uuid.uuid4().hex
71-
signals.request_started.send(sender=task_id)
72-
try:
73-
if release.build is not None:
74-
release.app.deploy(release, *args, **kwargs)
75-
release.state = "succeed"
76-
release.save()
77-
except Exception as e:
78-
release.state = "crashed"
79-
release.failed = True
80-
if release.summary:
81-
release.summary += " "
82-
release.summary += "{} deployed a config that failed".format(release.owner)
83-
# Get the exception that has occured
84-
release.exception = "error: {}".format(str(e))
85-
release.save()
86-
finally:
87-
signals.request_finished.send(sender=task_id)
88-
89-
9065
@shared_task(
9166
autoretry_for=(ServiceUnavailable, ),
9267
retry_jitter=True,

rootfs/api/tests/test_app.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,8 +235,8 @@ def test_run_without_release_should_error(self, mock_requests):
235235
body = {'command': 'ls -al'}
236236
response = self.client.post(url, body)
237237
self.assertEqual(response.status_code, 400, response.data)
238-
self.assertEqual(response.data, {'detail': 'No build associated with this '
239-
'release to run this command'})
238+
self.assertEqual(
239+
str(response.data["detail"]), 'no build available, please deploy a release')
240240

241241
@mock.patch('api.models.app.App.run', _mock_run)
242242
@mock.patch('api.models.app.App.deploy', mock_none)

rootfs/api/tests/test_app_settings.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,22 @@ def test_settings_routable(self, mock_requests):
4040
self.assertEqual(response.status_code, 201, response.data)
4141
self.assertFalse(app.appsettings_set.latest().routable)
4242

43+
def test_settings_autodeploy(self, mock_requests):
44+
"""
45+
Create an application with the autorollback flag turned on or off
46+
"""
47+
# create app, expecting autodeploy to be true
48+
app_id = self.create_app()
49+
app = App.objects.get(id=app_id)
50+
self.assertTrue(app.appsettings_set.latest().autodeploy)
51+
# Set autodeploy to false
52+
response = self.client.post(
53+
f'/v2/apps/{app.id}/settings',
54+
{'autodeploy': False}
55+
)
56+
self.assertEqual(response.status_code, 201, response.data)
57+
self.assertFalse(app.appsettings_set.latest().autodeploy)
58+
4359
def test_settings_autorollback(self, mock_requests):
4460
"""
4561
Create an application with the autorollback flag turned on or off

0 commit comments

Comments
 (0)