From eecc7576a7cf937c93eba2c11e84883c3e43b4e5 Mon Sep 17 00:00:00 2001 From: Cryptophobia Date: Thu, 22 Jun 2017 11:23:20 -0400 Subject: [PATCH 1/7] Adding initial changes to the api --- Makefile | 2 +- rootfs/api/migrations/0001_initial.py | 2 ++ rootfs/api/models/app.py | 2 ++ rootfs/api/models/config.py | 4 +++- rootfs/api/models/release.py | 34 +++++++++++++++++++++++++++ rootfs/api/serializers.py | 2 ++ 6 files changed, 44 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index ed689ab33..822d5865b 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,7 @@ docker-build: check-docker docker-build-test: check-docker docker build ${DOCKER_BUILD_FLAGS} -t ${IMAGE}.test -f rootfs/Dockerfile.test rootfs -deploy: check-kubectl docker-build docker-push +deploy: docker-build docker-push kubectl --namespace=deis patch deployment deis-$(COMPONENT) --type='json' -p='[{"op": "replace", "path": "/spec/template/spec/containers/0/image", "value":"$(IMAGE)"}]' clean: check-docker diff --git a/rootfs/api/migrations/0001_initial.py b/rootfs/api/migrations/0001_initial.py index 1ae2dd8fd..a969a7350 100644 --- a/rootfs/api/migrations/0001_initial.py +++ b/rootfs/api/migrations/0001_initial.py @@ -73,6 +73,8 @@ class Migration(migrations.Migration): ('updated', models.DateTimeField(auto_now=True)), ('values', jsonfield.fields.JSONField(default={}, blank=True)), ('memory', jsonfield.fields.JSONField(default={}, blank=True)), + ('lifecycle_post_start', jsonfield.fields.JSONField(default={}, blank=True)), + ('lifecycle_pre_stop', jsonfield.fields.JSONField(default={}, blank=True)), ('cpu', jsonfield.fields.JSONField(default={}, blank=True)), ('tags', jsonfield.fields.JSONField(default={}, blank=True)), ('app', models.ForeignKey(to='api.App')), diff --git a/rootfs/api/models/app.py b/rootfs/api/models/app.py index 9953f723d..e8f2bffba 100644 --- a/rootfs/api/models/app.py +++ b/rootfs/api/models/app.py @@ -1082,6 +1082,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..49ed4c1bd 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,7 @@ 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..35f355060 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_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 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) From ad405a1a84372641dba5c71f2f52dbc3138a45f3 Mon Sep 17 00:00:00 2001 From: Cryptophobia Date: Thu, 22 Jun 2017 14:45:44 -0400 Subject: [PATCH 2/7] Adding the django migration for lifecycle hooks --- rootfs/api/migrations/0001_initial.py | 2 -- .../migrations/0024_config_lifecycle_hooks.py | 26 +++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 rootfs/api/migrations/0024_config_lifecycle_hooks.py diff --git a/rootfs/api/migrations/0001_initial.py b/rootfs/api/migrations/0001_initial.py index a969a7350..1ae2dd8fd 100644 --- a/rootfs/api/migrations/0001_initial.py +++ b/rootfs/api/migrations/0001_initial.py @@ -73,8 +73,6 @@ class Migration(migrations.Migration): ('updated', models.DateTimeField(auto_now=True)), ('values', jsonfield.fields.JSONField(default={}, blank=True)), ('memory', jsonfield.fields.JSONField(default={}, blank=True)), - ('lifecycle_post_start', jsonfield.fields.JSONField(default={}, blank=True)), - ('lifecycle_pre_stop', jsonfield.fields.JSONField(default={}, blank=True)), ('cpu', jsonfield.fields.JSONField(default={}, blank=True)), ('tags', jsonfield.fields.JSONField(default={}, blank=True)), ('app', models.ForeignKey(to='api.App')), 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={}), + ), + ] From 5c1b73688b9435fcdb3af54f034456320bd53951 Mon Sep 17 00:00:00 2001 From: Cryptophobia Date: Thu, 29 Jun 2017 16:59:56 -0400 Subject: [PATCH 3/7] Final logic for the lifecycle hooks --- Makefile | 2 +- rootfs/api/models/release.py | 12 ++++----- rootfs/scheduler/resources/pod.py | 43 +++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index 822d5865b..ed689ab33 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,7 @@ docker-build: check-docker docker-build-test: check-docker docker build ${DOCKER_BUILD_FLAGS} -t ${IMAGE}.test -f rootfs/Dockerfile.test rootfs -deploy: docker-build docker-push +deploy: check-kubectl docker-build docker-push kubectl --namespace=deis patch deployment deis-$(COMPONENT) --type='json' -p='[{"op": "replace", "path": "/spec/template/spec/containers/0/image", "value":"$(IMAGE)"}]' clean: check-docker diff --git a/rootfs/api/models/release.py b/rootfs/api/models/release.py index 35f355060..330540b1e 100644 --- a/rootfs/api/models/release.py +++ b/rootfs/api/models/release.py @@ -441,17 +441,17 @@ def save(self, *args, **kwargs): # noqa self.summary += ' and ' self.summary += "{} {}".format(self.config.owner, changes) - # if the lifecycle_post_start hooks changed, log the dict diff + # if the lifecycle_pre_stop 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) + 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_post_start ' + added if added else '' + added = 'added lifecycle_pre_stop ' + added if added else '' changed = ', '.join(k for k in diff.get('changed', {})) - changed = 'changed lifecycle_post_start ' + changed if changed else '' + changed = 'changed lifecycle_pre_stop ' + changed if changed else '' deleted = ', '.join(k for k in diff.get('deleted', {})) - deleted = 'deleted lifecycle_post_start ' + deleted if deleted else '' + 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: diff --git a/rootfs/scheduler/resources/pod.py b/rootfs/scheduler/resources/pod.py index b8ad48c65..5e52163c3 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() From df2cd6bcd326b9525498673dfca2d711e3d7166c Mon Sep 17 00:00:00 2001 From: Madison Steiner Date: Tue, 25 Jul 2017 09:02:15 -0700 Subject: [PATCH 4/7] Fix spacing --- rootfs/api/models/config.py | 3 ++- rootfs/scheduler/resources/pod.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/rootfs/api/models/config.py b/rootfs/api/models/config.py index 49ed4c1bd..4ab1145e6 100644 --- a/rootfs/api/models/config.py +++ b/rootfs/api/models/config.py @@ -164,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', 'lifecycle_post_start', 'lifecycle_pre_stop']: + 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/scheduler/resources/pod.py b/rootfs/scheduler/resources/pod.py index 5e52163c3..f1c0e62b8 100644 --- a/rootfs/scheduler/resources/pod.py +++ b/rootfs/scheduler/resources/pod.py @@ -291,7 +291,7 @@ def _set_lifecycle_hooks(self, container, env, **kwargs): lifecycle = defaultdict(dict) if lifecycle_post_start: - lifecycle["postStart"] = { + lifecycle["postStart"] = { 'exec': { "command": [ "/bin/bash", @@ -301,7 +301,7 @@ def _set_lifecycle_hooks(self, container, env, **kwargs): } } if lifecycle_pre_stop: - lifecycle["preStop"] = { + lifecycle["preStop"] = { 'exec': { "command": [ "/bin/bash", From 94d6c66f30eb0b34ca40de69a405758fa17894b4 Mon Sep 17 00:00:00 2001 From: Cryptophobia Date: Tue, 25 Jul 2017 13:09:14 -0400 Subject: [PATCH 5/7] Fixing the broken config api tests --- rootfs/api/tests/test_config.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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, From 68ee0afd620659c7c6748c9d95d30eb6a3e5b3d8 Mon Sep 17 00:00:00 2001 From: Cryptophobia Date: Wed, 16 Aug 2017 14:18:27 -0400 Subject: [PATCH 6/7] Adding procfile_structure to app api --- .../migrations/0025_app_procfile_structure.py | 22 +++++++++++++++++++ rootfs/api/models/app.py | 13 +++++++++++ rootfs/api/serializers.py | 3 ++- 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 rootfs/api/migrations/0025_app_procfile_structure.py diff --git a/rootfs/api/migrations/0025_app_procfile_structure.py b/rootfs/api/migrations/0025_app_procfile_structure.py new file mode 100644 index 000000000..efee506e8 --- /dev/null +++ b/rootfs/api/migrations/0025_app_procfile_structure.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.2 on 2017-08-14 20:45 +from __future__ import unicode_literals + +import api.models.app +from django.db import migrations +import jsonfield.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0024_config_lifecycle_hooks'), + ] + + operations = [ + migrations.AddField( + model_name='app', + name='procfile_structure', + field=jsonfield.fields.JSONField(blank=True, default={}, validators=[api.models.app.validate_app_structure]), + ), + ] diff --git a/rootfs/api/models/app.py b/rootfs/api/models/app.py index 1e0c2eca5..cec6d9a3b 100644 --- a/rootfs/api/models/app.py +++ b/rootfs/api/models/app.py @@ -71,6 +71,7 @@ class App(UuidAuditedModel): validators=[validate_app_id, validate_reserved_names]) structure = JSONField(default={}, blank=True, validators=[validate_app_structure]) + procfile_structure = JSONField(default={}, blank=True, validators=[validate_app_structure]) class Meta: verbose_name = 'Application' @@ -408,6 +409,7 @@ def scale(self, user, structure): # noqa if new_structure != self.structure: # save new structure to the database self.structure = new_structure + self.procfile_structure = release.build.procfile self.save() try: @@ -474,6 +476,7 @@ def deploy(self, release, force_deploy=False, rollback_on_failure=True): # noqa # set processes structure to default if app is new. if self.structure == {}: self.structure = self._default_structure(release) + self.procfile_structure = self._default_structure(release) self.save() # reset canonical process types if build type has changed. else: @@ -489,8 +492,18 @@ def deploy(self, release, force_deploy=False, rollback_on_failure=True): # noqa # update with the default process type. structure.update(self._default_structure(release)) self.structure = structure + # if procfile structure exists then we use it + if release.build.procfile and \ + release.build.sha and not \ + release.build.dockerfile: + self.procfile_structure = release.build.procfile self.save() + # always set the procfile structure for any new release + if release.build.procfile: + self.procfile_structure = release.build.procfile + self.save() + # deploy application to k8s. Also handles initial scaling app_settings = self.appsettings_set.latest() deploys = {} diff --git a/rootfs/api/serializers.py b/rootfs/api/serializers.py index 3c5bbbe74..b91cc8681 100644 --- a/rootfs/api/serializers.py +++ b/rootfs/api/serializers.py @@ -172,11 +172,12 @@ class AppSerializer(serializers.ModelSerializer): owner = serializers.ReadOnlyField(source='owner.username') structure = serializers.JSONField(required=False) + procfile_structure = serializers.JSONField(required=False) class Meta: """Metadata options for a :class:`AppSerializer`.""" model = models.App - fields = ['uuid', 'id', 'owner', 'structure', 'created', 'updated'] + fields = ['uuid', 'id', 'owner', 'structure', 'procfile_structure', 'created', 'updated'] class BuildSerializer(serializers.ModelSerializer): From d93fb516ba1a4f0a5af1a2b5f44e2a6b055a29f2 Mon Sep 17 00:00:00 2001 From: Cryptophobia Date: Thu, 17 Aug 2017 14:44:28 -0400 Subject: [PATCH 7/7] Fixing the test_app.py failure --- rootfs/api/tests/test_app.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rootfs/api/tests/test_app.py b/rootfs/api/tests/test_app.py index 40dcb4197..5b4b3bf0f 100644 --- a/rootfs/api/tests/test_app.py +++ b/rootfs/api/tests/test_app.py @@ -90,7 +90,8 @@ def test_response_data(self, mock_requests): body = {'id': 'app-{}'.format(random.randrange(1000, 10000))} response = self.client.post('/v2/apps', body) for key in response.data: - self.assertIn(key, ['uuid', 'created', 'updated', 'id', 'owner', 'structure']) + self.assertIn(key, ['uuid', 'created', 'updated', 'id', 'owner', 'structure', + 'procfile_structure']) expected = { 'id': body['id'], 'owner': self.user.username,