Skip to content
This repository was archived by the owner on May 6, 2020. It is now read-only.

Adding procfile_structure on app api #1321

Closed
wants to merge 9 commits into from
26 changes: 26 additions & 0 deletions rootfs/api/migrations/0024_config_lifecycle_hooks.py
Original file line number Diff line number Diff line change
@@ -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={}),
),
]
22 changes: 22 additions & 0 deletions rootfs/api/migrations/0025_app_procfile_structure.py
Original file line number Diff line number Diff line change
@@ -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]),
),
]
15 changes: 15 additions & 0 deletions rootfs/api/models/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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 = {}
Expand Down Expand Up @@ -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,
Expand Down
5 changes: 4 additions & 1 deletion rootfs/api/models/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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()

Expand Down
34 changes: 34 additions & 0 deletions rootfs/api/models/release.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
Expand Down
5 changes: 4 additions & 1 deletion rootfs/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion rootfs/api/tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 4 additions & 2 deletions rootfs/api/tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
43 changes: 43 additions & 0 deletions rootfs/scheduler/resources/pod.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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":
Expand Down Expand Up @@ -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()
Expand Down