diff --git a/README.rst b/README.rst index 634684d..8ac1cf2 100644 --- a/README.rst +++ b/README.rst @@ -37,9 +37,13 @@ To run the script:: -r | --region AWS Region Name. May also be set as environment variable AWS_DEFAULT_REGION -p | --profile AWS Profile to use - If you set this aws-access-key, aws-secret-key and region are needed -c | --cluster Name of ECS cluster - -i | --image Name of Docker image to run, ex: repo/image:latest - Format: [domain][:port][/repo][/][image][:tag] - Examples: mariadb, mariadb:latest, silintl/mariadb, silintl/mariadb:latest, private.registry.com:8000/repo/image:tag + -l |--container-image CONTAINER_IMAGE + Name of the container with the name of Docker image to + run Format: [container_name]=[domain][:port][/repo][/] + [image][:tag] Examples: db=mariadb, + database=mariadb:latest, db=silintl/mariadb:latest, + my_container|private.registry.com:8000/repo/image:tag. + You can use this argument multiple times to replace Optional arguments: -D | --desired-count The number of instantiations of the task to place and keep running in your service. -m | --min minumumHealthyPercent: The lower limit on the number of running tasks during a deployment. diff --git a/ecs_deploy.py b/ecs_deploy.py index d038d5d..ab4ab52 100644 --- a/ecs_deploy.py +++ b/ecs_deploy.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python # -*- coding: utf-8 -*- """ ecs-deploy.py @@ -26,7 +27,9 @@ def __init__(self): 'aws_access_key_id') credentials = self._arg_kwargs(credentials, 'aws_secret_key', 'aws_secret_access_key') - credentials = self._arg_kwargs(credentials, 'aws_region', 'region') + credentials = self._arg_kwargs(credentials, 'aws_region', + 'region_name') + # init boto3 ecs client self.client = boto3.client('ecs', **credentials) except ClientError as err: @@ -77,7 +80,7 @@ def _init_parser(self): parser.add_argument( '-r', - '--region', + '--aws-region', help='AWS Region Name. May also be set as environment variable \ AWS_DEFAULT_REGION') @@ -103,13 +106,17 @@ def _init_parser(self): help='Name of ECS cluster') parser.add_argument( - '-i', - '--image', + '-l', + '--container-image', + action='append', required=True, - help='Name of Docker image to run, ex: repo/image:latest\nFormat: \ - [domain][:port][/repo][/][image][:tag]\nExamples: mariadb, \ - mariadb:latest, silintl/mariadb,\nsilintl/mariadb:latest, \ - private.registry.com:8000/repo/image:tag') + help='Name of the container with the name of Docker image to run\ + \nFormat: \ + [container_name]=[domain][:port][/repo][/][image][:tag]\ + \nExamples: db=mariadb, database=mariadb:latest, \ + db=silintl/mariadb:latest, \ + my_container|private.registry.com:8000/repo/image:tag. \ + You can use this argument multiple times to replace images.') # OPTIONAL ARGUMENTS @@ -166,32 +173,51 @@ def _run_parser(self): self.task_definition_name = self._task_definition_name() self.service_name = self._service_name() - self.task_definition = \ - self.client_fn('describe_task_definition')['taskDefinition'] - self.new_task_definition = \ - self.client_fn('register_task_definition')['taskDefinition'] + self.task_definition = self.client_fn( + 'describe_task_definition')['taskDefinition'] + print('Current task definition: %s' % + self.task_definition['taskDefinitionArn']) + + self.new_task_definition = self.client_fn( + 'register_task_definition')['taskDefinition'] + print('New task definition: %s' % + self.new_task_definition['taskDefinitionArn']) if self.task_definition: if not self.client_fn('update_service'): sys.exit(1) # loop for desired timeout - timeout = self.args.get('timeout') or time.time() + 90 + timeout = self._int_or_none(self.args.get('timeout')) or 90 + started_at = time.time() while True: - updated = False - running_tasks = self.client_fn('describe_tasks')['tasks'] - for task in running_tasks: + self.running_tasks = self.client_fn('list_tasks')['taskArns'] + + if len(self.running_tasks) > 0: + described_tasks = self.client_fn('describe_tasks')['tasks'] + else: + described_tasks = [] + + for task in described_tasks: if task['taskDefinitionArn'] == \ self.new_task_definition['taskDefinitionArn']: - print('SUCCESS') - updated = True - if updated or time.time() > timeout: + print('Service updated successfully, ' + 'new task definition running.') + return + if time.time() > started_at + timeout: + print('ERROR: New task definition not running' + 'within %d seconds' % timeout) sys.exit(1) time.sleep(1) else: sys.exit(1) + def _int_or_none(self, value): + if type(value) == str and value.isdigit(): + return int(value) + return None + def _task_definition_name(self): if self.args.get('task_definition'): return self.args.get('task_definition') @@ -235,10 +261,22 @@ def client_kwargs(self, fn): kwargs['family'] = self.task_definition['family'] kwargs['containerDefinitions'] = \ self.task_definition['containerDefinitions'] + # optional kwargs from args - if self.args.get('image'): - kwargs['containerDefinitions'][0]['image'] = \ - self.args.get('image') + for ci in self.args.get('container_image'): + ci_container_name, ci_image = ci.split('=') + container_found = False + + for cd in kwargs['containerDefinitions']: + if cd['name'] == ci_container_name: + cd['image'] = ci_image + container_found = True + + if not container_found: + print('Container %s not found in the task definition %s' % + (ci_container_name, + self.task_definition['taskDefinitionArn'])) + sys.exit(1) elif fn == 'update_service': kwargs['cluster'] = self.cluster @@ -260,7 +298,7 @@ def client_kwargs(self, fn): elif fn == 'describe_tasks': kwargs['cluster'] = self.cluster - kwargs['tasks'] = self.client_fn('list_tasks')['taskArns'] + kwargs['tasks'] = self.running_tasks return kwargs @@ -277,5 +315,10 @@ def client_fn(self, fn): print('Exception: %s' % e) sys.exit(1) -if __name__ == '__main__': + +def main(): CLI() + + +if __name__ == '__main__': + main() diff --git a/setup.py b/setup.py index 29aa512..a3db54c 100644 --- a/setup.py +++ b/setup.py @@ -60,9 +60,9 @@ def run_tests(self): setup( name='ecs-deploy-py', - version='0.1.3', + version='0.1.4', url='http://github.com/cuttlesoft/ecs-deploy.py', - download_url='https://github.com/cuttlesoft/ecs-deploy.py/tarball/0.1.3', + download_url='https://github.com/cuttlesoft/ecs-deploy.py/tarball/0.1.4', license='MIT', author='Cuttlesoft, LLC', author_email='engineering@cuttlesoft.com', @@ -74,14 +74,14 @@ def run_tests(self): zip_safe=False, platforms='any', install_requires=[ - 'boto3==1.4.0', - 'botocore==1.4.56', - 'docutils==0.12', - 'futures==3.0.5', - 'jmespath==0.9.0', - 'python-dateutil==2.5.3', - 's3transfer==0.1.4', - 'six==1.10.0' + 'boto3>=1.4.0', + 'botocore>=1.4.56', + 'docutils>=0.12', + 'futures>=3.0.5', + 'jmespath>=0.9.0', + 'python-dateutil>=2.5.3', + 's3transfer>=0.1.4', + 'six>=1.10.0' ], cmdclass={'test': PyTest}, classifiers=[ @@ -95,7 +95,7 @@ def run_tests(self): 'Topic :: Software Development :: Libraries :: Python Modules'], entry_points={ 'console_scripts': [ - 'ecs-deploy-py = ecs_deploy:CLI' + 'ecs-deploy-py = ecs_deploy:main' ] } ) diff --git a/tests/test_script.py b/tests/test_script.py index 9ea97a9..2e1e39c 100644 --- a/tests/test_script.py +++ b/tests/test_script.py @@ -1,4 +1,3 @@ -import pytest import mock import boto3 from moto import mock_ecs @@ -126,7 +125,7 @@ def setUp(self): 'cluster': mock_cluster['cluster']['clusterName'], 'task_definition': mock_task['taskDefinition']['family'], 'service_name': mock_service['service']['serviceName'], - 'image': 'mock_image' + 'container_image': ['string=mock_image'] } self.mock_cli.cluster = mock_cluster['cluster'] @@ -231,7 +230,7 @@ def test_client_kwargs_with_register_task_definition(self): assert 'family' in mock_kwargs assert 'containerDefinitions' in mock_kwargs assert mock_kwargs['containerDefinitions'][0]['image'] == \ - mock_cli.args['image'] + mock_cli.args['container_image'][0].split('=')[1] assert mock_kwargs['family'] == mock_cli.task_definition['family'] def test_client_kwargs_with_update_service(self): @@ -343,23 +342,17 @@ def test_client_kwargs_with_list_tasks(self): def test_client_kwargs_with_describe_tasks(self): mock_cli, client = self.setUp() mock_cli.service_name = mock_cli.args['service_name'] + mock_cli.running_tasks = [""] - with mock.patch.object(mock_cli, 'client_fn') as mock_fn: - mock_list_tasks_response = { - 'taskArns': [ - mock_cli.task_definition['taskDefinitionArn'] - ] - } - mock_fn.return_value = mock_list_tasks_response - mock_kwargs = mock_cli.client_kwargs('describe_tasks') + mock_kwargs = mock_cli.client_kwargs('describe_tasks') - assert 'cluster' in mock_kwargs - assert 'tasks' in mock_kwargs - assert mock_kwargs['cluster']['clusterName'] == \ - mock_cli.args['cluster'] - assert mock_kwargs['tasks'] == mock_list_tasks_response['taskArns'] + assert 'cluster' in mock_kwargs + assert 'tasks' in mock_kwargs + assert mock_kwargs['cluster']['clusterName'] == \ + mock_cli.args['cluster'] + assert mock_kwargs['tasks'] == mock_cli.running_tasks - def test_client_kwargs_with_describe_tasks(self): + def test_client_kwargs_with_list_services_once(self): mock_cli, client = self.setUp() mock_cli.client = client