88from __future__ import unicode_literals
99
1010import json
11- import logging
1211import requests
1312
1413from django .contrib .auth .models import User
1514from django .test import TransactionTestCase
16- import etcd
1715import mock
1816from rest_framework .authtoken .models import Token
1917
20- import api .exceptions
2118from api .models import App , Config
2219from . import mock_status_ok
2320
@@ -33,19 +30,6 @@ def mock_request_connection_error(*args, **kwargs):
3330 raise requests .exceptions .ConnectionError ("connection error" )
3431
3532
36- class MockEtcdClient :
37-
38- def __init__ (self , app ):
39- self .app = app
40-
41- def get (self , key , * args , ** kwargs ):
42- node = {
43- 'key' : '/deis/services/{}/{}_v2.web.1' .format (self .app , self .app ),
44- 'value' : '127.0.0.1:1234'
45- }
46- return etcd .EtcdResult (None , node )
47-
48-
4933@mock .patch ('api.models.publish_release' , lambda * args : None )
5034class ConfigTest (TransactionTestCase ):
5135
@@ -570,142 +554,3 @@ def test_unauthorized_user_cannot_modify_config(self):
570554 response = self .client .post (url , json .dumps (body ), content_type = 'application/json' ,
571555 HTTP_AUTHORIZATION = 'token {}' .format (unauthorized_token ))
572556 self .assertEqual (response .status_code , 403 )
573-
574- def _test_app_healthcheck (self ):
575- # post a new build, expecting it to pass as usual
576- url = "/v2/apps/{self.app}/builds" .format (** locals ())
577- body = {'image' : 'autotest/example' }
578- response = self .client .post (url , json .dumps (body ), content_type = 'application/json' ,
579- HTTP_AUTHORIZATION = 'token {}' .format (self .token ))
580- self .assertEqual (response .status_code , 201 )
581- # mock out the etcd client
582- api .models ._etcd_client = MockEtcdClient (self .app )
583- # set an initial healthcheck url.
584- url = "/v2/apps/{self.app}/config" .format (** locals ())
585- body = {'values' : json .dumps ({'HEALTHCHECK_URL' : '/' })}
586- return self .client .post (url , json .dumps (body ), content_type = 'application/json' ,
587- HTTP_AUTHORIZATION = 'token {}' .format (self .token ))
588-
589- @mock .patch ('requests.get' , mock_status_ok )
590- @mock .patch ('time.sleep' , lambda func : func )
591- def test_app_healthcheck_good (self ):
592- """
593- If a user deploys an app with a config value set for HEALTHCHECK_URL, the controller
594- should check that the application responds with a 200 OK.
595- """
596- response = self ._test_app_healthcheck ()
597- self .assertEqual (response .status_code , 201 )
598- self .assertEqual (self .app .release_set .latest ().version , 3 )
599-
600- @mock .patch ('requests.get' , mock_status_not_found )
601- @mock .patch ('api.models.get_etcd_client' , lambda func : func )
602- @mock .patch ('time.sleep' , lambda func : func )
603- @mock .patch ('api.models.logger' )
604- def test_app_healthcheck_bad (self , mock_logger ):
605- """
606- If a user deploys an app with a config value set for HEALTHCHECK_URL, the controller
607- should check that the application responds with a 200 OK. If it's down, the app should be
608- rolled back.
609- """
610- response = self ._test_app_healthcheck ()
611- self .assertEqual (response .status_code , 503 )
612- self .assertEqual (
613- response .data ,
614- {'detail' : 'aborting, app containers failed to respond to health check' })
615- # check that only the build and initial release exist
616- self .assertEqual (self .app .release_set .latest ().version , 2 )
617- # assert that the reason why the containers failed was because
618- # they failed the health check 4 times; we do this by looking
619- # at logs-- there may be a better way
620- exp_msg = "{}: app failed health check (got '404', expected: '200'); trying again in 0.0 \
621- seconds" .format (self .app .id )
622- exp_log_call = mock .call (logging .WARNING , exp_msg )
623- log_calls = mock_logger .log .mock_calls
624- self .assertEqual (log_calls .count (exp_log_call ), 3 )
625- exp_msg = "{}: app failed health check (got '404', expected: '200')" .format (self .app .id )
626- exp_log_call = mock .call (logging .WARNING , exp_msg )
627- self .assertEqual (log_calls .count (exp_log_call ), 1 )
628-
629- @mock .patch ('requests.get' , mock_status_not_found )
630- @mock .patch ('api.models.get_etcd_client' , lambda func : func )
631- @mock .patch ('time.sleep' )
632- def test_app_backoff_interval (self , mock_time ):
633- """
634- Ensure that when a healthcheck fails, a backoff strategy is used before trying again.
635- """
636- # post a new build, expecting it to pass as usual
637- url = "/v2/apps/{self.app}/builds" .format (** locals ())
638- body = {'image' : 'autotest/example' }
639- response = self .client .post (url , json .dumps (body ), content_type = 'application/json' ,
640- HTTP_AUTHORIZATION = 'token {}' .format (self .token ))
641- self .assertEqual (response .status_code , 201 )
642- # mock out the etcd client
643- api .models ._etcd_client = MockEtcdClient (self .app )
644- # set an initial healthcheck url.
645- url = "/v2/apps/{self.app}/config" .format (** locals ())
646- body = {'values' : json .dumps ({'HEALTHCHECK_URL' : '/' })}
647- return self .client .post (url , json .dumps (body ), content_type = 'application/json' ,
648- HTTP_AUTHORIZATION = 'token {}' .format (self .token ))
649- self .assertEqual (mock_time .call_count , 5 )
650-
651- @mock .patch ('requests.get' , mock_status_ok )
652- @mock .patch ('time.sleep' )
653- def test_app_healthcheck_initial_delay (self , mock_time ):
654- """
655- Ensure that when an initial delay is set, the request will sleep for x seconds, where
656- x is the number of seconds in the initial timeout.
657- """
658- # post a new build, expecting it to pass as usual
659- url = "/v2/apps/{self.app}/builds" .format (** locals ())
660- body = {'image' : 'autotest/example' }
661- response = self .client .post (url , json .dumps (body ), content_type = 'application/json' ,
662- HTTP_AUTHORIZATION = 'token {}' .format (self .token ))
663- self .assertEqual (response .status_code , 201 )
664- # mock out the etcd client
665- api .models ._etcd_client = MockEtcdClient (self .app )
666- # set an initial healthcheck url.
667- url = "/v2/apps/{self.app}/config" .format (** locals ())
668- body = {'values' : json .dumps ({'HEALTHCHECK_URL' : '/' })}
669- return self .client .post (url , json .dumps (body ), content_type = 'application/json' ,
670- HTTP_AUTHORIZATION = 'token {}' .format (self .token ))
671- # mock_time increments by one each time its called, so we should expect 2 calls to
672- # mock_time; one for the call in the code, and one for this invocation.
673- mock_time .assert_called_with (0 )
674- app = App .objects .all ()[0 ]
675- url = "/v2/apps/{app}/config" .format (** locals ())
676- body = {'values' : json .dumps ({'HEALTHCHECK_INITIAL_DELAY' : 10 })}
677- self .client .post (url , json .dumps (body ), content_type = 'application/json' ,
678- HTTP_AUTHORIZATION = 'token {}' .format (self .token ))
679- mock_time .assert_called_with (10 )
680-
681- @mock .patch ('requests.get' )
682- @mock .patch ('time.sleep' , lambda func : func )
683- def test_app_healthcheck_timeout (self , mock_request ):
684- """
685- Ensure when a timeout value is set, the controller respects that value
686- when making a request.
687- """
688- self ._test_app_healthcheck ()
689- app = App .objects .all ()[0 ]
690- url = "/v2/apps/{app}/config" .format (** locals ())
691- body = {'values' : json .dumps ({'HEALTHCHECK_TIMEOUT' : 10 })}
692- self .client .post (url , json .dumps (body ), content_type = 'application/json' ,
693- HTTP_AUTHORIZATION = 'token {}' .format (self .token ))
694- mock_request .assert_called_with ('http://127.0.0.1:1234/' , timeout = 10 )
695-
696- @mock .patch ('requests.get' , mock_request_connection_error )
697- @mock .patch ('time.sleep' , lambda func : func )
698- def test_app_healthcheck_connection_error (self ):
699- """
700- If a user deploys an app with a config value set for HEALTHCHECK_URL but the app
701- returns a connection error, the controller should continue checking until either the app
702- responds or the app fails to respond within the timeout.
703-
704- NOTE (bacongobbler): the Docker userland proxy listens for connections and returns a
705- ConnectionError, hence the unit test.
706- """
707- response = self ._test_app_healthcheck ()
708- self .assertEqual (response .status_code , 503 )
709- self .assertEqual (
710- response .data ,
711- {'detail' : 'aborting, app containers failed to respond to health check' })
0 commit comments