Skip to content

Commit af76733

Browse files
author
Matthew Fisher
authored
feat(api): Add healthcheck field to Config
1 parent 5cc99c5 commit af76733

8 files changed

Lines changed: 284 additions & 110 deletions

File tree

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# -*- coding: utf-8 -*-
2+
# Generated by Django 1.9.7 on 2016-06-17 20:18
3+
from __future__ import unicode_literals
4+
5+
from django.db import migrations
6+
import jsonfield.fields
7+
8+
9+
class Migration(migrations.Migration):
10+
11+
dependencies = [
12+
('api', '0009_auto_20160607_2259'),
13+
]
14+
15+
operations = [
16+
migrations.AddField(
17+
model_name='config',
18+
name='healthcheck',
19+
field=jsonfield.fields.JSONField(blank=True, default={}),
20+
),
21+
]

rootfs/api/models/app.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,7 @@ def _scale_pods(self, scale_types):
371371
'replicas': replicas,
372372
'app_type': scale_type,
373373
'build_type': release.build.type,
374-
'healthcheck': release.config.healthcheck(),
374+
'healthcheck': release.config.healthcheck,
375375
'routable': routable
376376
}
377377

@@ -427,7 +427,7 @@ def deploy(self, release):
427427
'version': "v{}".format(release.version),
428428
'app_type': scale_type,
429429
'build_type': release.build.type,
430-
'healthcheck': release.config.healthcheck(),
430+
'healthcheck': release.config.healthcheck,
431431
'routable': routable,
432432
'batches': batches
433433
}
@@ -503,11 +503,12 @@ def verify_application_health(self, **kwargs):
503503
# Get the router host and append healthcheck path
504504
url = 'http://{}:{}'.format(settings.ROUTER_HOST, settings.ROUTER_PORT)
505505

506-
# if a health check url is available then 200 is the only acceptable status code
507-
if len(kwargs['healthcheck']):
506+
# if a httpGet probe is available then 200 is the only acceptable status code
507+
if 'livenessProbe' in kwargs.get('healthcheck', {}) and 'httpGet' in kwargs.get('healthcheck').get('livenessProbe'): # noqa
508508
allowed = [200]
509-
url = urljoin(url, kwargs['healthcheck'].get('path'))
510-
req_timeout = kwargs['healthcheck'].get('timeout')
509+
handler = kwargs['healthcheck']['livenessProbe']['httpGet']
510+
url = urljoin(url, handler.get('path', '/'))
511+
req_timeout = handler.get('timeoutSeconds', 1)
511512
else:
512513
allowed = set(range(200, 599))
513514
allowed.remove(404)

rootfs/api/models/config.py

Lines changed: 15 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class Config(UuidAuditedModel):
2020
cpu = JSONField(default={}, blank=True)
2121
tags = JSONField(default={}, blank=True)
2222
registry = JSONField(default={}, blank=True)
23+
healthcheck = JSONField(default={}, blank=True)
2324

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

32-
def healthcheck(self):
33+
def _migrate_legacy_healthcheck(self):
3334
"""
3435
Get all healthchecks options together for use in scheduler
3536
"""
36-
# return empty dict if no healthcheck is found
37+
# return if no legacy healthcheck is found
3738
if 'HEALTHCHECK_URL' not in self.values.keys():
38-
return {}
39+
return
3940

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

47-
return {
48-
'path': path,
49-
'timeout': timeout,
50-
'delay': delay,
51-
'period_seconds': period_seconds,
52-
'success_threshold': success_threshold,
53-
'failure_threshold': failure_threshold,
48+
self.healthcheck['livenessProbe'] = {
49+
'initialDelaySeconds': delay,
50+
'timeoutSeconds': timeout,
51+
'periodSeconds': period_seconds,
52+
'successThreshold': success_threshold,
53+
'failureThreshold': failure_threshold,
54+
'httpGet': {
55+
'path': path,
56+
}
5457
}
5558

56-
def set_healthchecks(self):
57-
"""Defines default values for HTTP healthchecks"""
58-
if not {k: v for k, v in self.values.items() if k.startswith('HEALTHCHECK_')}:
59-
return
60-
61-
# fetch set health values and any defaults
62-
# this approach allows new health items to be added without issues
63-
health = self.healthcheck()
64-
if not health:
65-
return
66-
67-
# HTTP GET related
68-
self.values['HEALTHCHECK_URL'] = health['path']
69-
70-
# Number of seconds after which the probe times out.
71-
# More info: http://releases.k8s.io/HEAD/docs/user-guide/pod-states.md#container-probes
72-
self.values['HEALTHCHECK_TIMEOUT'] = health['timeout']
73-
# Number of seconds after the container has started before liveness probes are initiated.
74-
# More info: http://releases.k8s.io/HEAD/docs/user-guide/pod-states.md#container-probes
75-
self.values['HEALTHCHECK_INITIAL_DELAY'] = health['delay']
76-
# How often (in seconds) to perform the probe.
77-
self.values['HEALTHCHECK_PERIOD_SECONDS'] = health['period_seconds']
78-
# Minimum consecutive successes for the probe to be considered successful
79-
# after having failed.
80-
self.values['HEALTHCHECK_SUCCESS_THRESHOLD'] = health['success_threshold']
81-
# Minimum consecutive failures for the probe to be considered failed after
82-
# having succeeded.
83-
self.values['HEALTHCHECK_FAILURE_THRESHOLD'] = health['failure_threshold']
84-
8559
def set_registry(self):
8660
# lower case all registry options for consistency
8761
self.registry = {key.lower(): value for key, value in self.registry.copy().items()}
@@ -127,7 +101,7 @@ def save(self, **kwargs):
127101
# usually means a totally new app
128102
previous_config = self.app.config_set.latest()
129103

130-
for attr in ['cpu', 'memory', 'tags', 'registry', 'values']:
104+
for attr in ['cpu', 'memory', 'tags', 'registry', 'values', 'healthcheck']:
131105
data = getattr(previous_config, attr, {}).copy()
132106
new_data = getattr(self, attr, {}).copy()
133107

@@ -137,13 +111,12 @@ def save(self, **kwargs):
137111
# error if unsetting non-existing key
138112
if key not in data:
139113
raise UnprocessableEntity('{} does not exist under {}'.format(key, attr)) # noqa
140-
141114
data.pop(key)
142115
else:
143116
data[key] = value
144117
setattr(self, attr, data)
145118

146-
self.set_healthchecks()
119+
self._migrate_legacy_healthcheck()
147120
self.set_registry()
148121
self.set_tags(previous_config)
149122
except Config.DoesNotExist:

rootfs/api/models/release.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,23 @@ def save(self, *args, **kwargs): # noqa
420420
self.summary += ' and '
421421
self.summary += "{} {}".format(self.config.owner, changes)
422422

423+
# if the registry information changed, log the dict diff
424+
changes = []
425+
old_healthcheck = old_config.healthcheck if old_config else {}
426+
diff = dict_diff(self.config.healthcheck, old_healthcheck)
427+
# try to be as succinct as possible
428+
added = ', '.join(k for k in diff.get('added', {}))
429+
added = 'added healthcheck info ' + added if added else ''
430+
changed = ', '.join(k for k in diff.get('changed', {}))
431+
changed = 'changed healthcheck info ' + changed if changed else ''
432+
deleted = ', '.join(k for k in diff.get('deleted', {}))
433+
deleted = 'deleted healthcheck info ' + deleted if deleted else ''
434+
changes = ', '.join(i for i in (added, changed, deleted) if i)
435+
if changes:
436+
if self.summary:
437+
self.summary += ' and '
438+
self.summary += "{} {}".format(self.config.owner, changes)
439+
423440
if not self.summary:
424441
if self.version == 1:
425442
self.summary = "{} created the initial release".format(self.owner)

rootfs/api/serializers.py

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import json
66
import re
7+
import jsonschema
78
from urllib.parse import urlparse
89

910
from django.contrib.auth.models import User
@@ -19,10 +20,77 @@
1920
CPUSHARE_MATCH = re.compile(r'^(?P<cpu>[-+]?[0-9]*\.?[0-9]+[m]{0,1})$')
2021
TAGVAL_MATCH = re.compile(r'^(?:[a-zA-Z\d][-\.\w]{0,61})?[a-zA-Z\d]$')
2122
CONFIGKEY_MATCH = re.compile(r'^[a-z_]+[a-z0-9_]*$', re.IGNORECASE)
23+
PROBE_SCHEMA = {
24+
"$schema": "http://json-schema.org/schema#",
25+
26+
"type": "object",
27+
"properties": {
28+
# Exec specifies the action to take.
29+
# More info: http://kubernetes.io/docs/api-reference/v1/definitions/#_v1_execaction
30+
"exec": {
31+
"type": "object",
32+
"properties": {
33+
"command": {
34+
"type": "array",
35+
"minItems": 1,
36+
"items": {"type": "string"}
37+
}
38+
},
39+
"required": ["command"]
40+
},
41+
# HTTPGet specifies the http request to perform.
42+
# More info: http://kubernetes.io/docs/api-reference/v1/definitions/#_v1_httpgetaction
43+
"httpGet": {
44+
"type": "object",
45+
"properties": {
46+
"path": {"type": "string"},
47+
"port": {"type": "integer"},
48+
"host": {"type": "string"},
49+
"scheme": {"type": "string"},
50+
"httpHeaders": {
51+
"type": "array",
52+
"minItems": 0,
53+
"items": {
54+
"type": "object",
55+
"properties": {
56+
"name": {"type": "string"},
57+
"value": {"type": "string"},
58+
}
59+
}
60+
}
61+
},
62+
"required": ["port"]
63+
},
64+
# TCPSocket specifies an action involving a TCP port.
65+
# More info: http://kubernetes.io/docs/api-reference/v1/definitions/#_v1_tcpsocketaction
66+
"tcpSocket": {
67+
"type": "object",
68+
"properties": {
69+
"port": {"type": "integer"},
70+
},
71+
"required": ["port"]
72+
},
73+
# Number of seconds after the container has started before liveness probes are initiated.
74+
# More info: http://releases.k8s.io/HEAD/docs/user-guide/pod-states.md#container-probes
75+
"initialDelaySeconds": {"type": "integer"},
76+
# Number of seconds after which the probe times out.
77+
# More info: http://releases.k8s.io/HEAD/docs/user-guide/pod-states.md#container-probes
78+
"timeoutSeconds": {"type": "integer"},
79+
# How often (in seconds) to perform the probe.
80+
"periodSeconds": {"type": "integer"},
81+
# Minimum consecutive successes for the probe to be considered successful
82+
# after having failed.
83+
"successThreshold": {"type": "integer"},
84+
# Minimum consecutive failures for the probe to be considered
85+
# failed after having succeeded.
86+
"failureThreshold": {"type": "integer"},
87+
}
88+
}
2289

2390

2491
class JSONFieldSerializer(serializers.JSONField):
2592
def __init__(self, *args, **kwargs):
93+
self.convert_to_str = kwargs.pop('convert_to_str', True)
2694
super(JSONFieldSerializer, self).__init__(*args, **kwargs)
2795

2896
def to_internal_value(self, data):
@@ -40,7 +108,8 @@ def to_representation(self, obj):
40108
continue
41109

42110
try:
43-
obj[k] = str(v)
111+
if self.convert_to_str:
112+
obj[k] = str(v)
44113
except ValueError:
45114
obj[k] = v
46115
# Do nothing, the validator will catch this later
@@ -128,6 +197,7 @@ class ConfigSerializer(serializers.ModelSerializer):
128197
cpu = JSONFieldSerializer(required=False, binary=True)
129198
tags = JSONFieldSerializer(required=False, binary=True)
130199
registry = JSONFieldSerializer(required=False, binary=True)
200+
healthcheck = JSONFieldSerializer(convert_to_str=False, required=False, binary=True)
131201

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

253323
return data
254324

325+
def validate_healthcheck(self, data):
326+
for key, value in data.items():
327+
if value is None:
328+
continue
329+
330+
if key not in ['livenessProbe', 'readinessProbe']:
331+
raise serializers.ValidationError(
332+
"Healthcheck keys must be either livenessProbe or readinessProbe")
333+
try:
334+
jsonschema.validate(value, PROBE_SCHEMA)
335+
except jsonschema.ValidationError as e:
336+
raise serializers.ValidationError(
337+
"could not validate {}: {}".format(value, e.message))
338+
339+
return data
340+
255341

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

0 commit comments

Comments
 (0)