From 356f56f9aa6411fd07858dbb4716909bf8074ffa Mon Sep 17 00:00:00 2001 From: Andrew Huynh Date: Fri, 19 Jun 2015 10:28:32 -0700 Subject: [PATCH] Adding tests for different sidekick functionality --- .coveragerc | 3 + pytest.ini | 3 + run_tests.sh | 1 + sidekick.py | 121 +++++++++++++++++++++++++++-------------- tests/__init__.py | 0 tests/test_sidekick.py | 62 +++++++++++++++++++++ 6 files changed, 149 insertions(+), 41 deletions(-) create mode 100644 .coveragerc create mode 100644 pytest.ini create mode 100755 run_tests.sh create mode 100644 tests/__init__.py create mode 100644 tests/test_sidekick.py diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..e7735e7 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,3 @@ +[run] +omit=*.pyenvs*,*site-packages*,tests/* + diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..ec4dab4 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +python_paths = tests + diff --git a/run_tests.sh b/run_tests.sh new file mode 100755 index 0000000..e919227 --- /dev/null +++ b/run_tests.sh @@ -0,0 +1 @@ +py.test --cov . --cov-report html --capture=no \ No newline at end of file diff --git a/sidekick.py b/sidekick.py index 8fac011..5a9527f 100644 --- a/sidekick.py +++ b/sidekick.py @@ -3,7 +3,7 @@ # @Author: ahuynh # @Date: 2015-06-10 16:51:36 # @Last Modified by: ahuynh -# @Last Modified time: 2015-06-12 10:15:05 +# @Last Modified time: 2015-06-18 21:16:25 ''' The sidekick should essentially replace job of the following typical bash script that is used to announce a service to ETCD. @@ -75,6 +75,77 @@ def check_name( container, name ): return False +def find_matching_container( containers, args ): + ''' + Given the name of the container: + - Find the matching container + - Note the open ports for that container + - Generate a UUID based on the name, ip, and port + + Return a dictionary of generated URIs mapped to the UUID for each open + port, using the following format: + + UUID: { + 'ip': IP that was passed in via args.ip, + 'port': Open port, + 'uri': IP:PORT + } + ''' + # Find the matching container + matching = {} + for container in containers: + if not check_name( container, args.name ): + continue + + ports = public_ports( container ) + + # TODO: Handle multiple public ports + # Right now we grab the first port in the list and announce the + # server using that IP + if len( ports ) == 0: + raise Exception( 'Container has no public ports' ) + + for port in ports: + port = port[ 'PublicPort' ] + + # Create a UUID + m = hashlib.md5() + m.update( args.name.encode('utf-8') ) + m.update( args.ip.encode('utf-8') ) + m.update( str( port ).encode('utf-8') ) + uuid = m.hexdigest() + + # Store the details + uri = '{}:{}'.format( args.ip, port ) + matching[ uuid ] = { 'ip': args.ip, 'port': port, 'uri': uri } + + return matching + + +def health_check( service ): + ''' + Check the health of `service`. + + This is done using a socket to test if the specified PublicPort is + responding to requests. + ''' + healthy = False + + try: + s = socket.socket() + s.connect( ( service['ip'], service['port'] ) ) + except ConnectionRefusedError: + logger.error( 'tcp://{ip}:{port} health check FAILED'.format(**service) ) + healthy = False + else: + s.close() + logger.error( 'tcp://{ip}:{port} health check SUCCEEDED'.format(**service) ) + healthy = True + s.close() + + return healthy + + def public_ports( container ): ''' Return a list of public ports for ''' return list(filter( lambda x: 'PublicPort' in x, container['Ports'] )) @@ -90,8 +161,6 @@ def main(): logger.info( 'Using {}'.format( args.docker ) ) kwargs['base_url'] = args.docker - docker_client = Client(**kwargs) - # Connect to ECTD etcd_client = etcd.Client() @@ -99,36 +168,16 @@ def main(): logger.debug( 'Announcing to {}'.format( etcd_folder ) ) # Find the matching container + docker_client = Client(**kwargs) try: containers = docker_client.containers() - except Exception: + logger.error( containers ) + except Exception as e: + logger.error( e ) sys.exit( 'FAILURE - Unable to connect Docker. Is it running?' ) # Find the matching container - matching = {} - for container in containers: - if check_name( container, args.name ): - ports = public_ports( container ) - - # TODO: Handle multiple public ports - # Right now we grab the first port in the list and announce the - # server using that IP - if len( ports ) == 0: - raise Exception( 'Container has no public ports' ) - - for port in ports: - port = port[ 'PublicPort' ] - - # Create a UUID - m = hashlib.md5() - m.update( args.name.encode('utf-8') ) - m.update( args.ip.encode('utf-8') ) - m.update( str( port ).encode('utf-8') ) - uuid = m.hexdigest() - - # Store the details - uri = '{}:{}'.format( args.ip, port ) - matching[ uuid ] = { 'ip': args.ip, 'port': port, 'uri': uri } + matching = find_matching_container( containers, args ) # Main health checking loop while True: @@ -138,25 +187,15 @@ def main(): full_key = os.path.join( etcd_folder, key ) - try: - s = socket.socket() - s.connect( ( value['ip'], value['port'] ) ) - except ConnectionRefusedError: - logger.error( 'tcp://{ip}:{port} health check FAILED'.format(**value) ) - healthy = False - else: - s.close() - logger.error( 'tcp://{ip}:{port} health check SUCCEEDED'.format(**value) ) - healthy = True - s.close() + healthy = health_check( value ) try: if not healthy: # Remove this server from ETCD if it exists etcd_client.delete( full_key ) else: - # Announce this server to ETCD - etcd_client.set( full_key, value['uri'] ) + # Announce this server to ETCD + etcd_client.set( full_key, value['uri'] ) except etcd.EtcdException as e: logging.error( e ) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_sidekick.py b/tests/test_sidekick.py new file mode 100644 index 0000000..d8c3cbf --- /dev/null +++ b/tests/test_sidekick.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# @Author: ahuynh +# @Date: 2015-06-18 20:15:30 +# @Last Modified by: ahuynh +# @Last Modified time: 2015-06-18 21:07:08 +import unittest + +from collections import namedtuple +from sidekick import check_name, find_matching_container, health_check, public_ports + +# Used to test command line arguments +Args = namedtuple('Args', ['name', 'ip']) + + +class TestSidekick( unittest.TestCase ): + + def setUp( self ): + + self.args = Args( name='test', ip='localhost' ) + + self.container = { + 'Image': 'image:latest', + 'Ports': [{ + 'PrivatePort': 9200, + 'IP': '0.0.0.0', + 'Type': 'tcp', + 'PublicPort': 9200 }, { + 'PrivatePort': 9300, + 'IP': '0.0.0.0', + 'Type': 'tcp', + 'PublicPort': 9300}], + 'Created': 1427906382, + 'Names': ['/test'], + 'Status': 'Up 2 days'} + + def test_check_name( self ): + ''' Test `check_name` functionality ''' + self.assertTrue( check_name( self.container, 'test' ) ) + self.assertFalse( check_name( self.container, '/test' ) ) + + def test_find_matching_container( self ): + ''' Test `find_matching_container` functionality ''' + # Test a successful match + results = find_matching_container( [self.container], self.args ) + self.assertEqual( len( results.keys() ), 2 ) + + # Test an unsuccessful match + no_open_ports = dict( self.container ) + no_open_ports['Ports'] = [] + with self.assertRaises( Exception ): + find_matching_container( [no_open_ports], self.args ) + + def test_health_check( self ): + ''' Test `health_check` functionality ''' + results = find_matching_container( [self.container], self.args ) + for value in results.values(): + self.assertFalse( health_check( value ) ) + + def test_public_ports( self ): + ''' Test `public_ports` functionality ''' + self.assertEquals( len( public_ports( self.container ) ), 2 )