Skip to content

Commit ac5d056

Browse files
author
Matthew Fisher
committed
fix(controller): revert HTTP healthcheck feature
1 parent 075cebe commit ac5d056

8 files changed

Lines changed: 19 additions & 268 deletions

File tree

controller/api/models.py

Lines changed: 0 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -299,45 +299,6 @@ def _start_containers(self, to_add):
299299
if set([c.state for c in to_add]) != set(['up']):
300300
err = 'warning, some containers failed to start'
301301
log_event(self, err, logging.WARNING)
302-
# if the user specified a health check, try checking to see if it's running
303-
try:
304-
config = self.config_set.latest()
305-
if 'HEALTHCHECK_URL' in config.values.keys():
306-
# check the app to see if it's healthy
307-
try:
308-
self._do_healthcheck(config)
309-
except Exception as e:
310-
log_event(self, str(e), logging.ERROR)
311-
self._destroy_containers(to_add)
312-
raise
313-
except Config.DoesNotExist:
314-
pass
315-
316-
def _do_healthcheck(self, config):
317-
timeout = time.time() + 60 # 1 minute from now
318-
while True:
319-
if time.time() > timeout:
320-
raise RuntimeError(
321-
'app failed to respond to health check within 60 seconds of launch')
322-
try:
323-
response = requests.get(
324-
'http://{}{}'.format(
325-
self.url,
326-
config.values['HEALTHCHECK_URL']),
327-
timeout=5)
328-
expected_status_code = config.values['HEALTHCHECK_STATUS_CODE'] if \
329-
'HEALTHCHECK_STATUS_CODE' in config.values.keys() else \
330-
settings.DEIS_HEALTHCHECK_STATUS_CODE
331-
if str(response.status_code) != str(expected_status_code):
332-
err = "aborting, app failed health check (got '{}', expected: '{}')".format(
333-
response.status_code,
334-
expected_status_code)
335-
raise RuntimeError(err)
336-
break
337-
except requests.exceptions.ConnectionError:
338-
continue
339-
except requests.exceptions.Timeout:
340-
continue
341302

342303
def _restart_containers(self, to_restart):
343304
"""Restarts containers via the scheduler"""
@@ -1043,34 +1004,6 @@ def _etcd_purge_app(**kwargs):
10431004
pass
10441005

10451006

1046-
def _etcd_publish_config(**kwargs):
1047-
config = kwargs['instance']
1048-
# we purge all existing config when adding the newest instance. This is because
1049-
# deis config:unset would remove an existing value, but not delete the
1050-
# old config object
1051-
try:
1052-
_etcd_client.delete('/deis/services/{}/config'.format(config.app),
1053-
prevExist=True, dir=True, recursive=True)
1054-
except KeyError:
1055-
pass
1056-
if kwargs['created']:
1057-
for k, v in config.values.iteritems():
1058-
_etcd_client.write(
1059-
'/deis/services/{}/config/{}'.format(
1060-
config.app,
1061-
unicode(k).encode('utf-8').lower()),
1062-
unicode(v).encode('utf-8'))
1063-
1064-
1065-
def _etcd_purge_config(**kwargs):
1066-
config = kwargs['instance']
1067-
try:
1068-
_etcd_client.delete('/deis/services/{}/config'.format(config.app),
1069-
prevExist=True, dir=True, recursive=True)
1070-
except KeyError:
1071-
pass
1072-
1073-
10741007
def _etcd_publish_cert(**kwargs):
10751008
cert = kwargs['instance']
10761009
if kwargs['created']:
@@ -1136,5 +1069,3 @@ def create_auth_token(sender, instance=None, created=False, **kwargs):
11361069
post_delete.connect(_etcd_purge_app, sender=App, dispatch_uid='api.models')
11371070
post_save.connect(_etcd_publish_cert, sender=Certificate, dispatch_uid='api.models')
11381071
post_delete.connect(_etcd_purge_cert, sender=Certificate, dispatch_uid='api.models')
1139-
post_save.connect(_etcd_publish_config, sender=Config, dispatch_uid='api.models')
1140-
post_delete.connect(_etcd_purge_config, sender=Config, dispatch_uid='api.models')

controller/api/tests/test_config.py

Lines changed: 11 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -15,38 +15,16 @@
1515
from django.test import TransactionTestCase
1616
from rest_framework.authtoken.models import Token
1717

18-
from api.models import App, Config
18+
from api.models import Config
1919

2020

21-
def mock_status_ok(*args, **kwargs):
21+
def mock_import_repository_task(*args, **kwargs):
2222
resp = requests.Response()
2323
resp.status_code = 200
2424
resp._content_consumed = True
2525
return resp
2626

2727

28-
def mock_status_not_found(*args, **kwargs):
29-
resp = requests.Response()
30-
resp.status_code = 404
31-
resp._content_consumed = True
32-
return resp
33-
34-
35-
def mock_request_timed_out(*args, **kwargs):
36-
raise requests.exceptions.Timeout()
37-
38-
39-
def mock_request_connection_error(*args, **kwargs):
40-
raise requests.exceptions.ConnectionError()
41-
42-
43-
def mock_time(*args, **kwargs):
44-
if not hasattr(mock_time, "counter"):
45-
mock_time.counter = 0 # it doesn't exist yet, so initialize it
46-
mock_time.counter += 1
47-
return mock_time.counter
48-
49-
5028
class ConfigTest(TransactionTestCase):
5129

5230
"""Tests setting and updating config values"""
@@ -57,7 +35,7 @@ def setUp(self):
5735
self.user = User.objects.get(username='autotest')
5836
self.token = Token.objects.get(user=self.user).key
5937

60-
@mock.patch('requests.post', mock_status_ok)
38+
@mock.patch('requests.post', mock_import_repository_task)
6139
def test_config(self):
6240
"""
6341
Test that config is auto-created for a new app and that
@@ -129,7 +107,7 @@ def test_config(self):
129107
self.assertEqual(response.status_code, 405)
130108
return config5
131109

132-
@mock.patch('requests.post', mock_status_ok)
110+
@mock.patch('requests.post', mock_import_repository_task)
133111
def test_response_data(self):
134112
"""Test that the serialized response contains only relevant data."""
135113
body = {'id': 'test'}
@@ -154,7 +132,7 @@ def test_response_data(self):
154132
}
155133
self.assertDictContainsSubset(expected, response.data)
156134

157-
@mock.patch('requests.post', mock_status_ok)
135+
@mock.patch('requests.post', mock_import_repository_task)
158136
def test_config_set_same_key(self):
159137
"""
160138
Test that config sets on the same key function properly
@@ -178,7 +156,7 @@ def test_config_set_same_key(self):
178156
self.assertIn('PORT', response.data['values'])
179157
self.assertEqual(response.data['values']['PORT'], '5001')
180158

181-
@mock.patch('requests.post', mock_status_ok)
159+
@mock.patch('requests.post', mock_import_repository_task)
182160
def test_config_set_unicode(self):
183161
"""
184162
Test that config sets with unicode values are accepted.
@@ -209,14 +187,14 @@ def test_config_set_unicode(self):
209187
self.assertIn('INTEGER', response.data['values'])
210188
self.assertEqual(response.data['values']['INTEGER'], 1)
211189

212-
@mock.patch('requests.post', mock_status_ok)
190+
@mock.patch('requests.post', mock_import_repository_task)
213191
def test_config_str(self):
214192
"""Test the text representation of a node."""
215193
config5 = self.test_config()
216194
config = Config.objects.get(uuid=config5['uuid'])
217195
self.assertEqual(str(config), "{}-{}".format(config5['app'], config5['uuid'][:7]))
218196

219-
@mock.patch('requests.post', mock_status_ok)
197+
@mock.patch('requests.post', mock_import_repository_task)
220198
def test_admin_can_create_config_on_other_apps(self):
221199
"""If a non-admin creates an app, an administrator should be able to set config
222200
values for that app.
@@ -236,7 +214,7 @@ def test_admin_can_create_config_on_other_apps(self):
236214
self.assertIn('PORT', response.data['values'])
237215
return response
238216

239-
@mock.patch('requests.post', mock_status_ok)
217+
@mock.patch('requests.post', mock_import_repository_task)
240218
def test_limit_memory(self):
241219
"""
242220
Test that limit is auto-created for a new app and that
@@ -324,7 +302,7 @@ def test_limit_memory(self):
324302
self.assertEqual(response.status_code, 405)
325303
return limit4
326304

327-
@mock.patch('requests.post', mock_status_ok)
305+
@mock.patch('requests.post', mock_import_repository_task)
328306
def test_limit_cpu(self):
329307
"""
330308
Test that CPU limits can be set
@@ -395,7 +373,7 @@ def test_limit_cpu(self):
395373
self.assertEqual(response.status_code, 405)
396374
return limit4
397375

398-
@mock.patch('requests.post', mock_status_ok)
376+
@mock.patch('requests.post', mock_import_repository_task)
399377
def test_tags(self):
400378
"""
401379
Test that tags can be set on an application
@@ -499,73 +477,3 @@ def test_unauthorized_user_cannot_modify_config(self):
499477
response = self.client.post(url, json.dumps(body), content_type='application/json',
500478
HTTP_AUTHORIZATION='token {}'.format(unauthorized_token))
501479
self.assertEqual(response.status_code, 403)
502-
503-
def _test_app_healthcheck(self):
504-
url = '/v1/apps'
505-
response = self.client.post(url, HTTP_AUTHORIZATION='token {}'.format(self.token))
506-
self.assertEqual(response.status_code, 201)
507-
app_id = response.data['id']
508-
# post a new build, expecting it to pass as usual
509-
url = "/v1/apps/{app_id}/builds".format(**locals())
510-
body = {'image': 'autotest/example'}
511-
response = self.client.post(url, json.dumps(body), content_type='application/json',
512-
HTTP_AUTHORIZATION='token {}'.format(self.token))
513-
self.assertEqual(response.status_code, 201)
514-
# set an initial healthcheck url.
515-
url = "/v1/apps/{app_id}/config".format(**locals())
516-
body = {'values': json.dumps({'HEALTHCHECK_URL': '/'})}
517-
return self.client.post(url, json.dumps(body), content_type='application/json',
518-
HTTP_AUTHORIZATION='token {}'.format(self.token))
519-
520-
@mock.patch('requests.get', mock_status_not_found)
521-
def test_app_healthcheck_good(self):
522-
"""
523-
If a user deploys an app with a config value set for HEALTHCHECK_URL, the controller
524-
should check that the application is up. If it's down, the app should be rolled back.
525-
"""
526-
response = self._test_app_healthcheck()
527-
self.assertEqual(response.status_code, 503)
528-
self.assertEqual(
529-
response.data,
530-
{
531-
'detail':
532-
"aborting, app failed health check (got '404', expected: '200')"
533-
})
534-
# add in the expected app healthcheck status, which will result in a successful deployment
535-
app_id = App.objects.all()[0]
536-
url = "/v1/apps/{app_id}/config".format(**locals())
537-
body = {'values': json.dumps({'HEALTHCHECK_STATUS_CODE': '404'})}
538-
response = self.client.post(url, json.dumps(body), content_type='application/json',
539-
HTTP_AUTHORIZATION='token {}'.format(self.token))
540-
self.assertEqual(response.status_code, 201)
541-
542-
@mock.patch('requests.get', mock_request_timed_out)
543-
@mock.patch('time.time', mock_time)
544-
def test_app_healthcheck_timeout(self):
545-
"""
546-
If a user deploys an app with a config value set for HEALTHCHECK_URL but the app
547-
times out, the controller should continue checking until either the app
548-
responds or the app fails to respond within the timeout.
549-
"""
550-
response = self._test_app_healthcheck()
551-
self.assertEqual(response.status_code, 503)
552-
self.assertEqual(
553-
response.data,
554-
{'detail': 'app failed to respond to health check within 60 seconds of launch'})
555-
556-
@mock.patch('requests.get', mock_request_connection_error)
557-
@mock.patch('time.time', mock_time)
558-
def test_app_healthcheck_connection_error(self):
559-
"""
560-
If a user deploys an app with a config value set for HEALTHCHECK_URL but the app
561-
returns a connection error, the controller should continue checking until either the app
562-
responds or the app fails to respond within the timeout.
563-
564-
NOTE (bacongobbler): the Docker userland proxy listens for connections and returns a
565-
ConnectionError, hence the unit test.
566-
"""
567-
response = self._test_app_healthcheck()
568-
self.assertEqual(response.status_code, 503)
569-
self.assertEqual(
570-
response.data,
571-
{'detail': 'app failed to respond to health check within 60 seconds of launch'})

controller/deis/settings.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -290,9 +290,6 @@
290290
# names which apps cannot reserve for routing
291291
DEIS_RESERVED_NAMES = ['deis']
292292

293-
# the default expected status code for healthchecks
294-
DEIS_HEALTHCHECK_STATUS_CODE = '200'
295-
296293
# default scheduler settings
297294
SCHEDULER_MODULE = 'scheduler.mock'
298295
SCHEDULER_TARGET = '' # path to scheduler endpoint (e.g. /var/run/fleet.sock)

docs/managing_deis/backing_up_data.rst

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,6 @@ use in the ``export`` command should correspond to the IP of the host machine wh
212212
[a.save() for a in App.objects.all()]
213213
[d.save() for d in Domain.objects.all()]
214214
[c.save() for c in Certificate.objects.all()]
215-
[c.save() for c in Config.objects.all()]
216215
EOF
217216
$ exit
218217

docs/using_deis/config-application.rst

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -91,27 +91,6 @@ appname to the old one:
9191
an application, however you can work around this by pointing the @ record to the
9292
address of the load balancer (if any).
9393

94-
Custom Health Checks
95-
--------------------
96-
97-
By default, Deis checks if the container is running for health checks. You can change both
98-
the healthcheck URL and the expected status code by setting custom config:
99-
100-
.. code-block:: console
101-
102-
$ deis config:set HEALTHCHECK_URL=/404.html
103-
=== peachy-waxworks
104-
HEALTHCHECK_URL: /404.html
105-
$ deis config:set HEALTHCHECK_STATUS_CODE=404
106-
=== peachy-waxworks
107-
HEALTHCHECK_STATUS_CODE: 404
108-
HEALTHCHECK_URL: /404.html
109-
110-
If a new release does not pass the healthcheck, the application will be rolled back to the previous
111-
release. Beyond that, if an application container responds to a heartbeat check with a different
112-
status than is expected, the :ref:`router` will mark that container as down and stop sending
113-
requests to that container.
114-
11594
Track Changes
11695
-------------
11796
Each time a build or config change is made to your application, a new :ref:`release` is created.

publisher/server/publisher.go

Lines changed: 0 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"fmt"
55
"log"
66
"net"
7-
"net/http"
87
"regexp"
98
"strconv"
109
"sync"
@@ -116,19 +115,6 @@ func (s *Server) publishContainer(container *docker.APIContainers, ttl time.Dura
116115
port := strconv.Itoa(int(p.PublicPort))
117116
hostAndPort := s.host + ":" + port
118117
if s.IsPublishableApp(containerName) && s.IsPortOpen(hostAndPort) {
119-
configKey := fmt.Sprintf("/deis/services/%s/config/", appName)
120-
// check if the user specified an upcheck URL
121-
healthcheckURL := s.getEtcd(configKey + "healthcheck_url")
122-
var healthcheckStatusCode int
123-
healthcheckStatusCode, err := strconv.Atoi(s.getEtcd(configKey + "healthcheck_status_code"))
124-
if err != nil {
125-
healthcheckStatusCode = 200
126-
}
127-
if healthcheckURL != "" {
128-
if !s.HealthCheckOK("http://"+hostAndPort+healthcheckURL, healthcheckStatusCode) {
129-
continue
130-
}
131-
}
132118
s.setEtcd(keyPath, hostAndPort, uint64(ttl.Seconds()))
133119
safeMap.Lock()
134120
safeMap.data[container.ID] = appPath
@@ -183,19 +169,6 @@ func (s *Server) IsPortOpen(hostAndPort string) bool {
183169
return portOpen
184170
}
185171

186-
func (s *Server) HealthCheckOK(url string, expectedStatusCode int) bool {
187-
resp, err := http.Get(url)
188-
if err != nil {
189-
log.Printf("healthcheck failed for %s (%v)\n", url, err)
190-
return false
191-
}
192-
if resp.StatusCode == expectedStatusCode {
193-
return true
194-
}
195-
log.Printf("healthcheck failed for %s (expected %d, got %d)\n", url, expectedStatusCode, resp.StatusCode)
196-
return false
197-
}
198-
199172
// latestRunningVersion retrieves the highest version of the application published
200173
// to etcd. If no app has been published, returns 0.
201174
func latestRunningVersion(client *etcd.Client, appName string) int {
@@ -240,21 +213,6 @@ func max(n []int) int {
240213
return val
241214
}
242215

243-
// getEtcd retrieves the etcd key's value. Returns an empty string if the key was not found.
244-
func (s *Server) getEtcd(key string) string {
245-
if s.logLevel == "debug" {
246-
log.Println("get", key)
247-
}
248-
resp, err := s.EtcdClient.Get(key, false, false)
249-
if err != nil {
250-
return ""
251-
}
252-
if resp != nil && resp.Node != nil {
253-
return resp.Node.Value
254-
}
255-
return ""
256-
}
257-
258216
// setEtcd sets the corresponding etcd key with the value and ttl
259217
func (s *Server) setEtcd(key, value string, ttl uint64) {
260218
if _, err := s.EtcdClient.Set(key, value, ttl); err != nil {

0 commit comments

Comments
 (0)