diff --git a/.github/PULL_REQUEST_TEMPLATE b/.github/PULL_REQUEST_TEMPLATE index a77ccb28e..c2d62322d 100644 --- a/.github/PULL_REQUEST_TEMPLATE +++ b/.github/PULL_REQUEST_TEMPLATE @@ -4,5 +4,5 @@ requires deis/workflow#1234 requires deis/workflow-e2e#5678 -[docs]: https://github.com/deis/workflow -[e2e]: https://github.com/deis/workflow-e2e +[docs]: https://github.com/deisthree/workflow +[e2e]: https://github.com/deisthree/workflow-e2e diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..ce3dde90e --- /dev/null +++ b/.travis.yml @@ -0,0 +1,8 @@ +sudo: required +language: go +services: +- docker +script: make test +notifications: + slack: + secure: pWDCV3od8gxvzxh9DrOTvBL54XoCfWYhZZlwd2ZbyyOz6SS12Psg/ZuCT2253p4yMfF/LPlsz76mr7NgcCrMI0ReveTa/rnt3XBZtyY+1rlsQsy2oxgdAzbO587ENCQeMw2F/OWHaixMT8NDqxEqQd6xafK9Zmg6BeBjwgs7XfXKcR3WzNIuCO0ZG05+Yd0FIxmd/8Xm5tGiFEYr05+Ix6MLdF9MSCXZUPeu1EsYXhDljokLq49w63W1UMU10tm4t7VCEdaO+X9w6EJ5Ov8HDxb6L6IviUYY6+IGTZ01nwIoM6OrGQqfEAytYqgTKdehgQzQnAbLI6TW2wJ0twqEsLrlbTa4NW4j0KkazQJkN5kqcKYQvaeKJJhvJIG44Gi/u78pW3S6W7NU5DhrlE6bbxdIBHJW1vJBimkqu2oBNrO5ZoBB9MS9zflBsU5g/pQpVeHWMnWE8fcYDGa1PqAcr7q6wtdPsrVZhnHmmARN3PwZzIVVVsXbaIQG8VLC5grLGnwMf1Y1fz2nK3sVpCftvrYZT3G6CNAASo+eLOwYdZdiJ9jIS7WNLN1GtpIEvjeDt3QRqsDyH8YoAKUvY5h/v8IWPP/BaSwQbJwep4+Dj7xkpXX5/4wm4jEnVFV1p4xE0lD1AXvEMAVHtPhhggvscNhF9j6oeoPju6eTPcxG+5o= diff --git a/Makefile b/Makefile index ed689ab33..2e0d65a6e 100644 --- a/Makefile +++ b/Makefile @@ -58,7 +58,7 @@ test-functional: @echo "Implement functional tests in _tests directory" test-integration: - @echo "Check https://github.com/deis/workflow-e2e for the complete integration test suite" + @echo "Check https://github.com/deisthree/workflow-e2e for the complete integration test suite" upload-coverage: $(eval CI_ENV := $(shell curl -s https://codecov.io/env | bash)) diff --git a/README.md b/README.md index ea0c0a578..9417cf063 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,4 @@ -|![](https://upload.wikimedia.org/wikipedia/commons/thumb/1/17/Warning.svg/156px-Warning.svg.png) | Deis Workflow will soon no longer be maintained.
Please [read the announcement](https://deis.com/blog/2017/deis-workflow-final-release/) for more detail. | -|---:|---| -| 09/07/2017 | Deis Workflow [v2.18][] final release before entering maintenance mode | -| 03/01/2018 | End of Workflow maintenance: critical patches no longer merged | - # Deis Controller [![Build Status](https://ci.deis.io/job/controller/badge/icon)](https://ci.deis.io/job/controller) @@ -13,7 +8,7 @@ Deis (pronounced DAY-iss) Workflow is an open source Platform as a Service (PaaS) that adds a developer-friendly layer to any [Kubernetes](http://kubernetes.io) cluster, making it easy to deploy and manage applications on your own servers. -For more information about the Deis Workflow, please visit the main project page at https://github.com/deis/workflow. +For more information about the Deis Workflow, please visit the main project page at https://github.com/deisthree/workflow. We welcome your input! If you have feedback, please [submit an issue][issues]. If you'd like to participate in development, please read the "Development" section below and [submit a pull request][prs]. @@ -82,8 +77,8 @@ kubectl get pod --namespace=deis -w | grep deis-controller ``` [install-k8s]: https://kubernetes.io/docs/setup/pick-right-solution -[issues]: https://github.com/deis/controller/issues -[prs]: https://github.com/deis/controller/pulls -[workflow]: https://github.com/deis/workflow +[issues]: https://github.com/deisthree/controller/issues +[prs]: https://github.com/deisthree/controller/pulls +[workflow]: https://github.com/deisthree/workflow [Docker]: https://www.docker.com/ -[v2.18]: https://github.com/deis/workflow/releases/tag/v2.18.0 +[v2.18]: https://github.com/deisthree/workflow/releases/tag/v2.18.0 diff --git a/charts/controller/Chart.yaml b/charts/controller/Chart.yaml index 7071a1c51..b7928fe8b 100644 --- a/charts/controller/Chart.yaml +++ b/charts/controller/Chart.yaml @@ -1,5 +1,5 @@ name: controller -home: https://github.com/deis/controller +home: https://github.com/deisthree/controller version: description: Deis Workflow Controller (API). maintainers: diff --git a/charts/controller/templates/_helpers.tmpl b/charts/controller/templates/_helpers.tmpl index 0b9deb080..422a5ae3f 100644 --- a/charts/controller/templates/_helpers.tmpl +++ b/charts/controller/templates/_helpers.tmpl @@ -1,10 +1,12 @@ {{/* -Set apiVersion based on Kubernetes version +Set apiVersion based on .Capabilities.APIVersions */}} {{- define "rbacAPIVersion" -}} -{{- if ge .Capabilities.KubeVersion.Minor "6" -}} +{{- if .Capabilities.APIVersions.Has "rbac.authorization.k8s.io/v1beta1" -}} rbac.authorization.k8s.io/v1beta1 -{{- else -}} +{{- else if .Capabilities.APIVersions.Has "rbac.authorization.k8s.io/v1alpha1" -}} rbac.authorization.k8s.io/v1alpha1 +{{- else -}} +rbac.authorization.k8s.io/v1 {{- end -}} {{- end -}} diff --git a/charts/controller/templates/controller-clusterrole.yaml b/charts/controller/templates/controller-clusterrole.yaml index d0a08470a..53c934d72 100644 --- a/charts/controller/templates/controller-clusterrole.yaml +++ b/charts/controller/templates/controller-clusterrole.yaml @@ -34,7 +34,7 @@ rules: verbs: ["get"] - apiGroups: [""] resources: ["pods"] - verbs: ["get", "list", "delete"] + verbs: ["get", "list", "create", "delete"] - apiGroups: [""] resources: ["resourcequotas"] verbs: ["get", "create"] 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/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/migrations/0026_release_exception.py b/rootfs/api/migrations/0026_release_exception.py new file mode 100644 index 000000000..3f681cc7d --- /dev/null +++ b/rootfs/api/migrations/0026_release_exception.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.2 on 2017-08-28 14:59 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0025_app_procfile_structure'), + ] + + operations = [ + migrations.AddField( + model_name='release', + name='exception', + field=models.TextField(blank=True, null=True), + ), + ] diff --git a/rootfs/api/models/app.py b/rootfs/api/models/app.py index 219bb6c2c..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 = {} @@ -1086,6 +1099,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/build.py b/rootfs/api/models/build.py index 34e58d426..924b634b0 100644 --- a/rootfs/api/models/build.py +++ b/rootfs/api/models/build.py @@ -72,6 +72,8 @@ def create(self, user, *args, **kwargs): if 'new_release' in locals(): new_release.failed = True new_release.summary = "{} deployed {} which failed".format(self.owner, str(self.uuid)[:7]) # noqa + # Get the exception that has occured + new_release.exception = "error: {}".format(str(e)) new_release.save() else: self.delete() 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..8f98f86f5 100644 --- a/rootfs/api/models/release.py +++ b/rootfs/api/models/release.py @@ -24,6 +24,7 @@ class Release(UuidAuditedModel): version = models.PositiveIntegerField() summary = models.TextField(blank=True, null=True) failed = models.BooleanField(default=False) + exception = models.TextField(blank=True, null=True) config = models.ForeignKey('Config', on_delete=models.CASCADE) build = models.ForeignKey('Build', null=True, on_delete=models.CASCADE) @@ -243,6 +244,8 @@ def rollback(self, user, version=None): if 'new_release' in locals(): new_release.failed = True new_release.summary = "{} performed roll back to a release that failed".format(self.owner) # noqa + # Get the exception that has occured + new_release.exception = "error: {}".format(str(e)) new_release.save() raise DeisException(str(e)) from e @@ -424,6 +427,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..7e0a6358e 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): @@ -210,6 +211,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) @@ -244,7 +247,7 @@ def validate_values(self, data): if key == 'HEALTHCHECK_URL': # Only Path information is supported, not query / anchor or anything else # Path is the only thing Kubernetes supports right now - # See https://github.com/deis/controller/issues/774 + # See https://github.com/deisthree/controller/issues/774 uri = urlparse(value) if not uri.path: 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, diff --git a/rootfs/api/tests/test_app_settings.py b/rootfs/api/tests/test_app_settings.py index 27ef76d97..b587f5607 100644 --- a/rootfs/api/tests/test_app_settings.py +++ b/rootfs/api/tests/test_app_settings.py @@ -276,7 +276,7 @@ def test_settings_labels(self, mock_requests): base_labels = { 'label': { - 'git_repo': 'https://github.com/deis/controller', + 'git_repo': 'https://github.com/deisthree/controller', 'team': 'frontend', 'empty': '' } diff --git a/rootfs/api/tests/test_auth.py b/rootfs/api/tests/test_auth.py index 41d7efcea..9a826c0a5 100644 --- a/rootfs/api/tests/test_auth.py +++ b/rootfs/api/tests/test_auth.py @@ -378,5 +378,6 @@ def test_regenerate(self): def test_auth_no_ldap_by_default(self, mock_logger): """Ensure that LDAP authentication is disabled by default.""" self.test_auth() - # NOTE(bacongobbler): Using https://github.com/deis/controller/issues/1189 as a test case + # NOTE(bacongobbler): Using https://github.com/deisthree/controller/issues/1189 + # as a test case mock_logger.warning.assert_not_called() diff --git a/rootfs/api/tests/test_config.py b/rootfs/api/tests/test_config.py index c65721af3..160af515e 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, @@ -340,7 +342,7 @@ def test_admin_can_create_config_on_other_apps(self, mock_requests): def test_config_owner_is_requesting_user(self, mock_requests): """ Ensure that setting the config value is owned by the requesting user - See https://github.com/deis/deis/issues/2650 + See https://github.com/deisthree/deis/issues/2650 """ response = self.test_admin_can_create_config_on_other_apps() self.assertEqual(response.data['owner'], self.user.username) diff --git a/rootfs/api/tests/test_domain.py b/rootfs/api/tests/test_domain.py index 47d831eb7..9a024ab79 100644 --- a/rootfs/api/tests/test_domain.py +++ b/rootfs/api/tests/test_domain.py @@ -237,7 +237,7 @@ def test_delete_domain_does_not_exist(self): self.assertEqual(response.status_code, 404) def test_delete_domain_does_not_remove_latest(self): - """https://github.com/deis/deis/issues/3239""" + """https://github.com/deisthree/deis/issues/3239""" url = '/v2/apps/{app_id}/domains'.format(app_id=self.app_id) test_domains = [ 'test-domain.example.com', @@ -255,7 +255,7 @@ def test_delete_domain_does_not_remove_latest(self): Domain.objects.get(domain=test_domains[0]) def test_delete_domain_does_not_remove_others(self): - """https://github.com/deis/deis/issues/3475""" + """https://github.com/deisthree/deis/issues/3475""" self.test_delete_domain_does_not_remove_latest() self.assertEqual(Domain.objects.all().count(), 2) diff --git a/rootfs/api/tests/test_limits.py b/rootfs/api/tests/test_limits.py index d7b1f44aa..144c7bef8 100644 --- a/rootfs/api/tests/test_limits.py +++ b/rootfs/api/tests/test_limits.py @@ -64,7 +64,7 @@ def test_request_limit_memory(self, mock_requests): self.assertEqual(response.status_code, 200, response.data) self.assertIn('memory', response.data) self.assertEqual(response.data['memory'], {}) - # regression test for https://github.com/deis/deis/issues/1563 + # regression test for https://github.com/deisthree/deis/issues/1563 self.assertNotIn('"', response.data['memory']) # set an initial limit @@ -105,7 +105,7 @@ def test_request_limit_memory(self, mock_requests): self.assertIn('web', memory) self.assertEqual(memory['web'], '1G') - # regression test for https://github.com/deis/deis/issues/1613 + # regression test for https://github.com/deisthree/deis/issues/1613 # ensure that config:set doesn't wipe out previous limits body = {'values': json.dumps({'NEW_URL2': 'http://localhost:8080/'})} response = self.client.post(url, body) @@ -193,7 +193,7 @@ def test_request_limit_cpu(self, mock_requests): self.assertEqual(response.status_code, 200, response.data) self.assertIn('cpu', response.data) self.assertEqual(response.data['cpu'], {}) - # regression test for https://github.com/deis/deis/issues/1563 + # regression test for https://github.com/deisthree/deis/issues/1563 self.assertNotIn('"', response.data['cpu']) # set an initial limit diff --git a/rootfs/api/tests/test_pods.py b/rootfs/api/tests/test_pods.py index 978c97240..ecc48ca93 100644 --- a/rootfs/api/tests/test_pods.py +++ b/rootfs/api/tests/test_pods.py @@ -325,7 +325,7 @@ def test_container_str(self, mock_requests): self.assertRegex(pod['name'], app_id + '-(worker|web)-[0-9]{8,10}-[a-z0-9]{5}') def test_pod_command_format(self, mock_requests): - # regression test for https://github.com/deis/deis/pull/1285 + # regression test for https://github.com/deisthree/deis/pull/1285 app_id = self.create_app() # post a new build @@ -580,7 +580,7 @@ def test_run_command_good(self, mock_requests): def test_run_not_fail_on_debug(self, mock_requests): """ - do a run with DEIS_DEBUG on - https://github.com/deis/controller/issues/583 + do a run with DEIS_DEBUG on - https://github.com/deisthree/controller/issues/583 """ env = EnvironmentVarGuard() env['DEIS_DEBUG'] = 'true' diff --git a/rootfs/api/tests/test_release.py b/rootfs/api/tests/test_release.py index 71e3d231b..cdd5eedb4 100644 --- a/rootfs/api/tests/test_release.py +++ b/rootfs/api/tests/test_release.py @@ -114,7 +114,7 @@ def test_response_data(self, mock_requests): response = self.client.get(url) for key in response.data.keys(): self.assertIn(key, ['uuid', 'owner', 'created', 'updated', 'app', 'build', 'config', - 'summary', 'version', 'failed']) + 'summary', 'version', 'failed', 'exception']) expected = { 'owner': self.user.username, 'app': app_id, diff --git a/rootfs/api/tests/test_tls.py b/rootfs/api/tests/test_tls.py index aba25aca0..fb09d95a0 100644 --- a/rootfs/api/tests/test_tls.py +++ b/rootfs/api/tests/test_tls.py @@ -66,7 +66,7 @@ def test_tls_created_on_app_create(self, mock_requests): """ Ensure that a TLS object is created for an App with default values. - See https://github.com/deis/controller/issues/1042 + See https://github.com/deisthree/controller/issues/1042 """ app_id = self.create_app() response = self.client.get('/v2/apps/{}/tls'.format(app_id)) diff --git a/rootfs/api/views.py b/rootfs/api/views.py index 86c6624db..6866fca10 100644 --- a/rootfs/api/views.py +++ b/rootfs/api/views.py @@ -289,6 +289,8 @@ def post_save(self, config): if hasattr(self, 'release'): self.release.failed = True self.release.summary = "{} deployed a config that failed".format(self.request.user) # noqa + # Get the exception that has occured + self.release.exception = "error: {}".format(str(e)) self.release.save() else: config.delete() diff --git a/rootfs/dev_requirements.txt b/rootfs/dev_requirements.txt index 4d4b2df0a..0281c4cf9 100644 --- a/rootfs/dev_requirements.txt +++ b/rootfs/dev_requirements.txt @@ -9,7 +9,7 @@ codecov==2.0.9 # mock out python-requests, mostly k8s # requests-mock==1.3.0 -git+https://github.com/deis/requests-mock.git@class_adapter#egg=request_mock +git+https://github.com/deisthree/requests-mock.git@class_adapter#egg=request_mock # tail a log and pipe into tbgrep to find all tracebacks tbgrep==0.3.0 diff --git a/rootfs/scheduler/resources/pod.py b/rootfs/scheduler/resources/pod.py index b8ad48c65..0d3ada9ee 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": @@ -290,11 +324,11 @@ def _default_readiness_probe(self, container, build_type, port=None): http://kubernetes.io/docs/user-guide/pod-states/#container-probes /runner/init is the entry point of the slugrunner. - https://github.com/deis/slugrunner/blob/01eac53f1c5f1d1dfa7570bbd6b9e45c00441fea/rootfs/Dockerfile#L20 + https://github.com/deisthree/slugrunner/blob/01eac53f1c5f1d1dfa7570bbd6b9e45c00441fea/rootfs/Dockerfile#L20 Once it downloads the slug it starts running using `exec` which means the pid 1 will point to the slug/application command instead of entry point once the application has started. - https://github.com/deis/slugrunner/blob/01eac53f1c5f1d1dfa7570bbd6b9e45c00441fea/rootfs/runner/init#L90 + https://github.com/deisthree/slugrunner/blob/01eac53f1c5f1d1dfa7570bbd6b9e45c00441fea/rootfs/runner/init#L90 This should be added only for the build pack apps when a custom liveness probe is not set to make sure that the pod is ready only when the slug is downloaded and started running. @@ -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()