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

Commit

Permalink
feat(api): Add healthcheck field to Config
Browse files Browse the repository at this point in the history
  • Loading branch information
Matthew Fisher committed Jun 27, 2016
1 parent 5cc99c5 commit af76733
Show file tree
Hide file tree
Showing 8 changed files with 284 additions and 110 deletions.
21 changes: 21 additions & 0 deletions rootfs/api/migrations/0010_config_healthcheck.py
@@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-06-17 20:18
from __future__ import unicode_literals

from django.db import migrations
import jsonfield.fields


class Migration(migrations.Migration):

dependencies = [
('api', '0009_auto_20160607_2259'),
]

operations = [
migrations.AddField(
model_name='config',
name='healthcheck',
field=jsonfield.fields.JSONField(blank=True, default={}),
),
]
13 changes: 7 additions & 6 deletions rootfs/api/models/app.py
Expand Up @@ -371,7 +371,7 @@ def _scale_pods(self, scale_types):
'replicas': replicas,
'app_type': scale_type,
'build_type': release.build.type,
'healthcheck': release.config.healthcheck(),
'healthcheck': release.config.healthcheck,
'routable': routable
}

Expand Down Expand Up @@ -427,7 +427,7 @@ def deploy(self, release):
'version': "v{}".format(release.version),
'app_type': scale_type,
'build_type': release.build.type,
'healthcheck': release.config.healthcheck(),
'healthcheck': release.config.healthcheck,
'routable': routable,
'batches': batches
}
Expand Down Expand Up @@ -503,11 +503,12 @@ def verify_application_health(self, **kwargs):
# Get the router host and append healthcheck path
url = 'http://{}:{}'.format(settings.ROUTER_HOST, settings.ROUTER_PORT)

# if a health check url is available then 200 is the only acceptable status code
if len(kwargs['healthcheck']):
# if a httpGet probe is available then 200 is the only acceptable status code
if 'livenessProbe' in kwargs.get('healthcheck', {}) and 'httpGet' in kwargs.get('healthcheck').get('livenessProbe'): # noqa
allowed = [200]
url = urljoin(url, kwargs['healthcheck'].get('path'))
req_timeout = kwargs['healthcheck'].get('timeout')
handler = kwargs['healthcheck']['livenessProbe']['httpGet']
url = urljoin(url, handler.get('path', '/'))
req_timeout = handler.get('timeoutSeconds', 1)
else:
allowed = set(range(200, 599))
allowed.remove(404)
Expand Down
57 changes: 15 additions & 42 deletions rootfs/api/models/config.py
Expand Up @@ -20,6 +20,7 @@ class Config(UuidAuditedModel):
cpu = JSONField(default={}, blank=True)
tags = JSONField(default={}, blank=True)
registry = JSONField(default={}, blank=True)
healthcheck = JSONField(default={}, blank=True)

class Meta:
get_latest_by = 'created'
Expand All @@ -29,13 +30,13 @@ class Meta:
def __str__(self):
return "{}-{}".format(self.app.id, str(self.uuid)[:7])

def healthcheck(self):
def _migrate_legacy_healthcheck(self):
"""
Get all healthchecks options together for use in scheduler
"""
# return empty dict if no healthcheck is found
# return if no legacy healthcheck is found
if 'HEALTHCHECK_URL' not in self.values.keys():
return {}
return

path = self.values.get('HEALTHCHECK_URL', '/')
timeout = int(self.values.get('HEALTHCHECK_TIMEOUT', 50))
Expand All @@ -44,44 +45,17 @@ def healthcheck(self):
success_threshold = int(self.values.get('HEALTHCHECK_SUCCESS_THRESHOLD', 1))
failure_threshold = int(self.values.get('HEALTHCHECK_FAILURE_THRESHOLD', 3))

return {
'path': path,
'timeout': timeout,
'delay': delay,
'period_seconds': period_seconds,
'success_threshold': success_threshold,
'failure_threshold': failure_threshold,
self.healthcheck['livenessProbe'] = {
'initialDelaySeconds': delay,
'timeoutSeconds': timeout,
'periodSeconds': period_seconds,
'successThreshold': success_threshold,
'failureThreshold': failure_threshold,
'httpGet': {
'path': path,
}
}

def set_healthchecks(self):
"""Defines default values for HTTP healthchecks"""
if not {k: v for k, v in self.values.items() if k.startswith('HEALTHCHECK_')}:
return

# fetch set health values and any defaults
# this approach allows new health items to be added without issues
health = self.healthcheck()
if not health:
return

# HTTP GET related
self.values['HEALTHCHECK_URL'] = health['path']

# Number of seconds after which the probe times out.
# More info: http://releases.k8s.io/HEAD/docs/user-guide/pod-states.md#container-probes
self.values['HEALTHCHECK_TIMEOUT'] = health['timeout']
# Number of seconds after the container has started before liveness probes are initiated.
# More info: http://releases.k8s.io/HEAD/docs/user-guide/pod-states.md#container-probes
self.values['HEALTHCHECK_INITIAL_DELAY'] = health['delay']
# How often (in seconds) to perform the probe.
self.values['HEALTHCHECK_PERIOD_SECONDS'] = health['period_seconds']
# Minimum consecutive successes for the probe to be considered successful
# after having failed.
self.values['HEALTHCHECK_SUCCESS_THRESHOLD'] = health['success_threshold']
# Minimum consecutive failures for the probe to be considered failed after
# having succeeded.
self.values['HEALTHCHECK_FAILURE_THRESHOLD'] = health['failure_threshold']

def set_registry(self):
# lower case all registry options for consistency
self.registry = {key.lower(): value for key, value in self.registry.copy().items()}
Expand Down Expand Up @@ -127,7 +101,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', 'healthcheck']:
data = getattr(previous_config, attr, {}).copy()
new_data = getattr(self, attr, {}).copy()

Expand All @@ -137,13 +111,12 @@ def save(self, **kwargs):
# error if unsetting non-existing key
if key not in data:
raise UnprocessableEntity('{} does not exist under {}'.format(key, attr)) # noqa

data.pop(key)
else:
data[key] = value
setattr(self, attr, data)

self.set_healthchecks()
self._migrate_legacy_healthcheck()
self.set_registry()
self.set_tags(previous_config)
except Config.DoesNotExist:
Expand Down
17 changes: 17 additions & 0 deletions rootfs/api/models/release.py
Expand Up @@ -420,6 +420,23 @@ def save(self, *args, **kwargs): # noqa
self.summary += ' and '
self.summary += "{} {}".format(self.config.owner, changes)

# if the registry information changed, log the dict diff
changes = []
old_healthcheck = old_config.healthcheck if old_config else {}
diff = dict_diff(self.config.healthcheck, old_healthcheck)
# try to be as succinct as possible
added = ', '.join(k for k in diff.get('added', {}))
added = 'added healthcheck info ' + added if added else ''
changed = ', '.join(k for k in diff.get('changed', {}))
changed = 'changed healthcheck info ' + changed if changed else ''
deleted = ', '.join(k for k in diff.get('deleted', {}))
deleted = 'deleted healthcheck info ' + 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 not self.summary:
if self.version == 1:
self.summary = "{} created the initial release".format(self.owner)
Expand Down
88 changes: 87 additions & 1 deletion rootfs/api/serializers.py
Expand Up @@ -4,6 +4,7 @@

import json
import re
import jsonschema
from urllib.parse import urlparse

from django.contrib.auth.models import User
Expand All @@ -19,10 +20,77 @@
CPUSHARE_MATCH = re.compile(r'^(?P<cpu>[-+]?[0-9]*\.?[0-9]+[m]{0,1})$')
TAGVAL_MATCH = re.compile(r'^(?:[a-zA-Z\d][-\.\w]{0,61})?[a-zA-Z\d]$')
CONFIGKEY_MATCH = re.compile(r'^[a-z_]+[a-z0-9_]*$', re.IGNORECASE)
PROBE_SCHEMA = {
"$schema": "http://json-schema.org/schema#",

"type": "object",
"properties": {
# Exec specifies the action to take.
# More info: http://kubernetes.io/docs/api-reference/v1/definitions/#_v1_execaction
"exec": {
"type": "object",
"properties": {
"command": {
"type": "array",
"minItems": 1,
"items": {"type": "string"}
}
},
"required": ["command"]
},
# HTTPGet specifies the http request to perform.
# More info: http://kubernetes.io/docs/api-reference/v1/definitions/#_v1_httpgetaction
"httpGet": {
"type": "object",
"properties": {
"path": {"type": "string"},
"port": {"type": "integer"},
"host": {"type": "string"},
"scheme": {"type": "string"},
"httpHeaders": {
"type": "array",
"minItems": 0,
"items": {
"type": "object",
"properties": {
"name": {"type": "string"},
"value": {"type": "string"},
}
}
}
},
"required": ["port"]
},
# TCPSocket specifies an action involving a TCP port.
# More info: http://kubernetes.io/docs/api-reference/v1/definitions/#_v1_tcpsocketaction
"tcpSocket": {
"type": "object",
"properties": {
"port": {"type": "integer"},
},
"required": ["port"]
},
# Number of seconds after the container has started before liveness probes are initiated.
# More info: http://releases.k8s.io/HEAD/docs/user-guide/pod-states.md#container-probes
"initialDelaySeconds": {"type": "integer"},
# Number of seconds after which the probe times out.
# More info: http://releases.k8s.io/HEAD/docs/user-guide/pod-states.md#container-probes
"timeoutSeconds": {"type": "integer"},
# How often (in seconds) to perform the probe.
"periodSeconds": {"type": "integer"},
# Minimum consecutive successes for the probe to be considered successful
# after having failed.
"successThreshold": {"type": "integer"},
# Minimum consecutive failures for the probe to be considered
# failed after having succeeded.
"failureThreshold": {"type": "integer"},
}
}


class JSONFieldSerializer(serializers.JSONField):
def __init__(self, *args, **kwargs):
self.convert_to_str = kwargs.pop('convert_to_str', True)
super(JSONFieldSerializer, self).__init__(*args, **kwargs)

def to_internal_value(self, data):
Expand All @@ -40,7 +108,8 @@ def to_representation(self, obj):
continue

try:
obj[k] = str(v)
if self.convert_to_str:
obj[k] = str(v)
except ValueError:
obj[k] = v
# Do nothing, the validator will catch this later
Expand Down Expand Up @@ -128,6 +197,7 @@ class ConfigSerializer(serializers.ModelSerializer):
cpu = JSONFieldSerializer(required=False, binary=True)
tags = JSONFieldSerializer(required=False, binary=True)
registry = JSONFieldSerializer(required=False, binary=True)
healthcheck = JSONFieldSerializer(convert_to_str=False, required=False, binary=True)

class Meta:
"""Metadata options for a :class:`ConfigSerializer`."""
Expand Down Expand Up @@ -252,6 +322,22 @@ def validate_registry(self, data):

return data

def validate_healthcheck(self, data):
for key, value in data.items():
if value is None:
continue

if key not in ['livenessProbe', 'readinessProbe']:
raise serializers.ValidationError(
"Healthcheck keys must be either livenessProbe or readinessProbe")
try:
jsonschema.validate(value, PROBE_SCHEMA)
except jsonschema.ValidationError as e:
raise serializers.ValidationError(
"could not validate {}: {}".format(value, e.message))

return data


class ReleaseSerializer(serializers.ModelSerializer):
"""Serialize a :class:`~api.models.Release` model."""
Expand Down

0 comments on commit af76733

Please sign in to comment.