diff --git a/rootfs/api/migrations/0024_config_lifecycle_hooks.py b/rootfs/api/migrations/0024_config_lifecycle_hooks.py new file mode 100644 index 000000000..c23d80f9c --- /dev/null +++ b/rootfs/api/migrations/0024_config_lifecycle_hooks.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.2 on 2017-06-22 18:42 +from __future__ import unicode_literals + +from django.db import migrations +import jsonfield.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0023_app_k8s_name_length'), + ] + + operations = [ + migrations.AddField( + model_name='config', + name='lifecycle_post_start', + field=jsonfield.fields.JSONField(blank=True, default={}), + ), + migrations.AddField( + model_name='config', + name='lifecycle_pre_stop', + field=jsonfield.fields.JSONField(blank=True, default={}), + ), + ] diff --git a/rootfs/api/models/app.py b/rootfs/api/models/app.py index 219bb6c2c..1e0c2eca5 100644 --- a/rootfs/api/models/app.py +++ b/rootfs/api/models/app.py @@ -1086,6 +1086,8 @@ def _gather_app_settings(self, release, app_settings, process_type, replicas): 'app_type': process_type, 'build_type': release.build.type, 'healthcheck': healthcheck, + 'lifecycle_post_start': config.lifecycle_post_start, + 'lifecycle_pre_stop': config.lifecycle_pre_stop, 'routable': routable, 'deploy_batches': batches, 'deploy_timeout': deploy_timeout, diff --git a/rootfs/api/models/config.py b/rootfs/api/models/config.py index 2e69006d5..4ab1145e6 100644 --- a/rootfs/api/models/config.py +++ b/rootfs/api/models/config.py @@ -18,6 +18,8 @@ class Config(UuidAuditedModel): app = models.ForeignKey('App', on_delete=models.CASCADE) values = JSONField(default={}, blank=True) memory = JSONField(default={}, blank=True) + lifecycle_post_start = JSONField(default={}, blank=True) + lifecycle_pre_stop = JSONField(default={}, blank=True) cpu = JSONField(default={}, blank=True) tags = JSONField(default={}, blank=True) registry = JSONField(default={}, blank=True) @@ -162,7 +164,8 @@ def save(self, **kwargs): # usually means a totally new app previous_config = self.app.config_set.latest() - for attr in ['cpu', 'memory', 'tags', 'registry', 'values']: + for attr in ['cpu', 'memory', 'tags', 'registry', 'values', + 'lifecycle_post_start', 'lifecycle_pre_stop']: data = getattr(previous_config, attr, {}).copy() new_data = getattr(self, attr, {}).copy() diff --git a/rootfs/api/models/release.py b/rootfs/api/models/release.py index 1ccbbd436..330540b1e 100644 --- a/rootfs/api/models/release.py +++ b/rootfs/api/models/release.py @@ -424,6 +424,40 @@ def save(self, *args, **kwargs): # noqa changes = 'changed limits for '+', '.join(changes) self.summary += "{} {}".format(self.config.owner, changes) + # if the lifecycle_post_start hooks changed, log the dict diff + changes = [] + old_lifecycle_post_start = old_config.lifecycle_post_start if old_config else {} + diff = dict_diff(self.config.lifecycle_post_start, old_lifecycle_post_start) + # try to be as succinct as possible + added = ', '.join(k for k in diff.get('added', {})) + added = 'added lifecycle_post_start ' + added if added else '' + changed = ', '.join(k for k in diff.get('changed', {})) + changed = 'changed lifecycle_post_start ' + changed if changed else '' + deleted = ', '.join(k for k in diff.get('deleted', {})) + deleted = 'deleted lifecycle_post_start ' + deleted if deleted else '' + changes = ', '.join(i for i in (added, changed, deleted) if i) + if changes: + if self.summary: + self.summary += ' and ' + self.summary += "{} {}".format(self.config.owner, changes) + + # if the lifecycle_pre_stop hooks changed, log the dict diff + changes = [] + old_lifecycle_pre_stop = old_config.lifecycle_pre_stop if old_config else {} + diff = dict_diff(self.config.lifecycle_pre_stop, old_lifecycle_pre_stop) + # try to be as succinct as possible + added = ', '.join(k for k in diff.get('added', {})) + added = 'added lifecycle_pre_stop ' + added if added else '' + changed = ', '.join(k for k in diff.get('changed', {})) + changed = 'changed lifecycle_pre_stop ' + changed if changed else '' + deleted = ', '.join(k for k in diff.get('deleted', {})) + deleted = 'deleted lifecycle_pre_stop ' + deleted if deleted else '' + changes = ', '.join(i for i in (added, changed, deleted) if i) + if changes: + if self.summary: + self.summary += ' and ' + self.summary += "{} {}".format(self.config.owner, changes) + # if the tags changed, log the dict diff changes = [] old_tags = old_config.tags if old_config else {} diff --git a/rootfs/api/serializers.py b/rootfs/api/serializers.py index 46a46fb77..3c5bbbe74 100644 --- a/rootfs/api/serializers.py +++ b/rootfs/api/serializers.py @@ -210,6 +210,8 @@ class ConfigSerializer(serializers.ModelSerializer): owner = serializers.ReadOnlyField(source='owner.username') values = JSONFieldSerializer(required=False, binary=True) memory = JSONFieldSerializer(required=False, binary=True) + lifecycle_post_start = JSONFieldSerializer(required=False, binary=True) + lifecycle_pre_stop = JSONFieldSerializer(required=False, binary=True) cpu = JSONFieldSerializer(required=False, binary=True) tags = JSONFieldSerializer(required=False, binary=True) registry = JSONFieldSerializer(required=False, binary=True) diff --git a/rootfs/api/tests/test_config.py b/rootfs/api/tests/test_config.py index c65721af3..08f2a0834 100644 --- a/rootfs/api/tests/test_config.py +++ b/rootfs/api/tests/test_config.py @@ -165,7 +165,8 @@ def test_response_data(self, mock_requests): response = self.client.post(url, body) for key in response.data: self.assertIn(key, ['uuid', 'owner', 'created', 'updated', 'app', 'values', 'memory', - 'cpu', 'tags', 'registry', 'healthcheck']) + 'cpu', 'tags', 'registry', 'healthcheck', 'lifecycle_post_start', + 'lifecycle_pre_stop']) expected = { 'owner': self.user.username, 'app': app_id, @@ -188,7 +189,8 @@ def test_response_data_types_converted(self, mock_requests): self.assertEqual(response.status_code, 201, response.data) for key in response.data: self.assertIn(key, ['uuid', 'owner', 'created', 'updated', 'app', 'values', 'memory', - 'cpu', 'tags', 'registry', 'healthcheck']) + 'cpu', 'tags', 'registry', 'healthcheck', 'lifecycle_post_start', + 'lifecycle_pre_stop']) expected = { 'owner': self.user.username, 'app': app_id, diff --git a/rootfs/scheduler/resources/pod.py b/rootfs/scheduler/resources/pod.py index b8ad48c65..f1c0e62b8 100644 --- a/rootfs/scheduler/resources/pod.py +++ b/rootfs/scheduler/resources/pod.py @@ -224,6 +224,8 @@ def _set_container(self, namespace, container_name, data, **kwargs): self._set_health_checks(data, env, **kwargs) + self._set_lifecycle_hooks(data, env, **kwargs) + def _set_resources(self, container, kwargs): """ Set CPU/memory resource management manifest """ app_type = kwargs.get("app_type") @@ -278,6 +280,38 @@ def _set_health_checks(self, container, env, **kwargs): elif kwargs.get('routable', False): self._default_readiness_probe(container, kwargs.get('build_type'), env.get('PORT', None)) # noqa + def _set_lifecycle_hooks(self, container, env, **kwargs): + app_type = kwargs.get("app_type") + lifecycle_post_start = kwargs.get('lifecycle_post_start', {}) + lifecycle_post_start = lifecycle_post_start.get(app_type) + lifecycle_pre_stop = kwargs.get('lifecycle_pre_stop', {}) + lifecycle_pre_stop = lifecycle_pre_stop.get(app_type) + lifecycle = defaultdict(dict) + if lifecycle_post_start or lifecycle_pre_stop: + lifecycle = defaultdict(dict) + + if lifecycle_post_start: + lifecycle["postStart"] = { + 'exec': { + "command": [ + "/bin/bash", + "-c", + "{0}".format(lifecycle_post_start) + ] + } + } + if lifecycle_pre_stop: + lifecycle["preStop"] = { + 'exec': { + "command": [ + "/bin/bash", + "-c", + "{0}".format(lifecycle_pre_stop) + ] + } + } + container["lifecycle"] = dict(lifecycle) + def _default_readiness_probe(self, container, build_type, port=None): # Update only the application container with the health check if build_type == "buildpack": @@ -345,6 +379,15 @@ def _default_dockerapp_readiness_probe(self, port, delay=5, timeout=5, period_se } return readinessprobe + def _set_custom_termination_period(self, container, period_seconds=900): + """ + Applies a custom terminationGracePeriod only if provided as env variable. + """ + terminationperiod = { + 'terminationGracePeriodSeconds': int(period_seconds) + } + container.update(terminationperiod) + def delete(self, namespace, name): # get timeout info from pod pod = self.pod.get(namespace, name).json()