diff --git a/.travis.yml b/.travis.yml index 9f02b29..4b25a86 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,8 @@ language: python python: - "2.7" - "3.5" -install: pip install tox-travis + - "3.6" +install: pip install tox-travis boto3 script: tox after_script: - pip install scrutinizer-ocular diff --git a/README.rst b/README.rst index 4411379..a223199 100644 --- a/README.rst +++ b/README.rst @@ -253,6 +253,26 @@ Or implicitly via environment variables ``NEW_RELIC_API_KEY`` and ``NEW_RELIC_AP Optionally you can provide an additional comment to the deployment via ``--comment "New feature X"`` and the name of the user who deployed with ``--user john.doe`` +Troubleshooting +--------------- +If the service configuration in ECS is not optimally set, you might be seeing +timeout or other errors during the deployment. + +Timeout +======= +The timeout error means, that AWS ECS takes longer for the full deployment cycle then ecs-deploy is told to wait. The deployment itself still might finish successfully, if there are no other problems with the deployed containers. + +You can increase the time (in seconds) to wait for finishing the deployment via the ``--timeout`` parameter. This time includes the full cycle of stopping all old containers and (re)starting all new containers. Different stacks require different timeout values, the default is 300 seconds. + +The overall deployment time depends on different things: + +- the type of the application. For example node.js containers tend to take a long time to get stopped. But nginx containers tend to stop immediately, etc. +- are old and new containers able to run in parallel (e.g. using dynamic ports)? +- the deployment options and strategy (Maximum percent > 100)? +- the desired count of running tasks, compared to +- the number of ECS instances in the cluster + + Alternative Implementation -------------------------- There are some other libraries/tools available on GitHub, which also handle the deployment of containers in AWS ECS. If you prefer another language over Python, have a look at these projects: diff --git a/ecs_deploy/cli.py b/ecs_deploy/cli.py index 1704bdf..55180a3 100644 --- a/ecs_deploy/cli.py +++ b/ecs_deploy/cli.py @@ -8,11 +8,12 @@ from datetime import datetime, timedelta from ecs_deploy.ecs import DeployAction, ScaleAction, RunAction, EcsClient, \ - TaskPlacementError -from ecs_deploy.newrelic import Deployment + TaskPlacementError, EcsError, VERSION +from ecs_deploy.newrelic import Deployment, NewRelicException @click.group() +@click.version_option(version=VERSION, prog_name='ecs-deploy') def ecs(): # pragma: no cover pass @@ -24,49 +25,26 @@ def get_client(access_key_id, secret_access_key, region, profile): @click.command() @click.argument('cluster') @click.argument('service') -@click.option('-t', '--tag', - help='Changes the tag for ALL container images') -@click.option('-i', '--image', type=(str, str), multiple=True, - help='Overwrites the image for a container: ' - ' ') -@click.option('-c', '--command', type=(str, str), multiple=True, - help='Overwrites the command in a container: ' - ' ') -@click.option('-e', '--env', type=(str, str, str), multiple=True, - help='Adds or changes an environment variable: ' - ' ') -@click.option('-r', '--role', type=str, - help='Sets the task\'s role ARN: ') -@click.option('--task', type=str, - help='Task definition to be deployed. Can be a task ARN ' - 'or a task family with optional revision') -@click.option('--region', required=False, - help='AWS region (e.g. eu-central-1)') -@click.option('--access-key-id', required=False, - help='AWS access key id') -@click.option('--secret-access-key', required=False, - help='AWS secret access key') -@click.option('--profile', required=False, - help='AWS configuration profile name') -@click.option('--timeout', required=False, default=300, type=int, - help='Amount of seconds to wait for deployment before ' - 'command fails (default: 300)') -@click.option('--ignore-warnings', is_flag=True, - help='Do not fail deployment on warnings (port already in use ' - 'or insufficient memory/CPU)') -@click.option('--newrelic-apikey', required=False, - help='New Relic API Key for recording the deployment') -@click.option('--newrelic-appid', required=False, - help='New Relic App ID for recording the deployment') -@click.option('--comment', required=False, - help='Description/comment for recording the deployment') -@click.option('--user', required=False, - help='User who executes the deployment (used for recording)') -@click.option('--diff/--no-diff', default=True, - help='Print what values were changed in the task definition') -def deploy(cluster, service, tag, image, command, env, role, task, region, - access_key_id, secret_access_key, profile, timeout, newrelic_apikey, - newrelic_appid, comment, user, ignore_warnings, diff): +@click.option('-t', '--tag', help='Changes the tag for ALL container images') +@click.option('-i', '--image', type=(str, str), multiple=True, help='Overwrites the image for a container: ') +@click.option('-c', '--command', type=(str, str), multiple=True, help='Overwrites the command in a container: ') +@click.option('-e', '--env', type=(str, str, str), multiple=True, help='Adds or changes an environment variable: ') +@click.option('-r', '--role', type=str, help='Sets the task\'s role ARN: ') +@click.option('--task', type=str, help='Task definition to be deployed. Can be a task ARN or a task family with optional revision') +@click.option('--region', required=False, help='AWS region (e.g. eu-central-1)') +@click.option('--access-key-id', required=False, help='AWS access key id') +@click.option('--secret-access-key', required=False, help='AWS secret access key') +@click.option('--profile', required=False, help='AWS configuration profile name') +@click.option('--timeout', required=False, default=300, type=int, help='Amount of seconds to wait for deployment before command fails (default: 300)') +@click.option('--ignore-warnings', is_flag=True, help='Do not fail deployment on warnings (port already in use or insufficient memory/CPU)') +@click.option('--newrelic-apikey', required=False, help='New Relic API Key for recording the deployment') +@click.option('--newrelic-appid', required=False, help='New Relic App ID for recording the deployment') +@click.option('--comment', required=False, help='Description/comment for recording the deployment') +@click.option('--user', required=False, help='User who executes the deployment (used for recording)') +@click.option('--diff/--no-diff', default=True, help='Print which values were changed in the task definition (default: --diff)') +@click.option('--deregister/--no-deregister', default=True, help='Deregister or keep the old task definition (default: --deregister)') +@click.option('--rollback/--no-rollback', default=False, help='Rollback to previous revision, if deployment failed (default: --no-rollback)') +def deploy(cluster, service, tag, image, command, env, role, task, region, access_key_id, secret_access_key, profile, timeout, newrelic_apikey, newrelic_appid, comment, user, ignore_warnings, diff, deregister, rollback): """ Redeploy or modify a service. @@ -83,12 +61,7 @@ def deploy(cluster, service, tag, image, command, env, role, task, region, client = get_client(access_key_id, secret_access_key, region, profile) deployment = DeployAction(client, cluster, service) - if task: - td = deployment.get_task_definition(task) - click.secho('Deploying based on task definition: %s' % task) - else: - td = deployment.get_current_task_definition(deployment.service) - + td = get_task_definition(deployment, task) td.set_images(tag, **{key: value for (key, value) in image}) td.set_commands(**{key: value for (key, value) in command}) td.set_environment(env) @@ -97,35 +70,32 @@ def deploy(cluster, service, tag, image, command, env, role, task, region, if diff: print_diff(td) - click.secho('Creating new task definition revision') - new_td = deployment.update_task_definition(td) + new_td = create_task_definition(deployment, td) + + try: + deploy_task_definition( + deployment=deployment, + task_definition=new_td, + title='Deploying new task definition', + success_message='Deployment successful', + failure_message='Deployment failed', + timeout=timeout, + deregister=deregister, + previous_task_definition=td, + ignore_warnings=ignore_warnings, + ) - click.secho( - 'Successfully created revision: %d' % new_td.revision, - fg='green' - ) - click.secho( - 'Successfully deregistered revision: %d\n' % td.revision, - fg='green' - ) + except TaskPlacementError as e: + if rollback: + click.secho('%s\n' % str(e), fg='red', err=True) + rollback_task_definition(deployment, td, new_td) + exit(1) + else: + raise record_deployment(tag, newrelic_apikey, newrelic_appid, comment, user) - click.secho('Updating service') - deployment.deploy(new_td) - click.secho('Successfully changed task definition to: %s:%s\n' % - (new_td.family, new_td.revision), fg='green') - - wait_for_finish( - action=deployment, - timeout=timeout, - title='Deploying task definition', - success_message='Deployment successful', - failure_message='Deployment failed', - ignore_warnings=ignore_warnings - ) - - except Exception as e: + except (EcsError, NewRelicException) as e: click.secho('%s\n' % str(e), fg='red', err=True) exit(1) @@ -134,21 +104,13 @@ def deploy(cluster, service, tag, image, command, env, role, task, region, @click.argument('cluster') @click.argument('service') @click.argument('desired_count', type=int) -@click.option('--region', - help='AWS region (e.g. eu-central-1)') -@click.option('--access-key-id', - help='AWS access key id') -@click.option('--secret-access-key', - help='AWS secret access key') -@click.option('--profile', - help='AWS configuration profile name') -@click.option('--timeout', default=300, type=int, - help='AWS configuration profile') -@click.option('--ignore-warnings', is_flag=True, - help='Do not fail deployment on warnings (port already in use ' - 'or insufficient memory/CPU)') -def scale(cluster, service, desired_count, access_key_id, secret_access_key, - region, profile, timeout, ignore_warnings): +@click.option('--region', help='AWS region (e.g. eu-central-1)') +@click.option('--access-key-id', help='AWS access key id') +@click.option('--secret-access-key', help='AWS secret access key') +@click.option('--profile', help='AWS configuration profile name') +@click.option('--timeout', default=300, type=int, help='AWS configuration profile') +@click.option('--ignore-warnings', is_flag=True, help='Do not fail deployment on warnings (port already in use or insufficient memory/CPU)') +def scale(cluster, service, desired_count, access_key_id, secret_access_key, region, profile, timeout, ignore_warnings): """ Scale a service up or down. @@ -175,7 +137,7 @@ def scale(cluster, service, desired_count, access_key_id, secret_access_key, ignore_warnings=ignore_warnings ) - except Exception as e: + except EcsError as e: click.secho('%s\n' % str(e), fg='red', err=True) exit(1) @@ -184,24 +146,14 @@ def scale(cluster, service, desired_count, access_key_id, secret_access_key, @click.argument('cluster') @click.argument('task') @click.argument('count', required=False, default=1) -@click.option('-c', '--command', type=(str, str), multiple=True, - help='Overwrites the command in a container: ' - ' ') -@click.option('-e', '--env', type=(str, str, str), multiple=True, - help='Adds or changes an environment variable: ' - ' ') -@click.option('--region', - help='AWS region (e.g. eu-central-1)') -@click.option('--access-key-id', - help='AWS access key id') -@click.option('--secret-access-key', - help='AWS secret access key') -@click.option('--profile', - help='AWS configuration profile name') -@click.option('--diff/--no-diff', default=True, - help='Print what values were changed in the task definition') -def run(cluster, task, count, command, env, region, access_key_id, - secret_access_key, profile, diff): +@click.option('-c', '--command', type=(str, str), multiple=True, help='Overwrites the command in a container: ') +@click.option('-e', '--env', type=(str, str, str), multiple=True, help='Adds or changes an environment variable: ') +@click.option('--region', help='AWS region (e.g. eu-central-1)') +@click.option('--access-key-id', help='AWS access key id') +@click.option('--secret-access-key', help='AWS secret access key') +@click.option('--profile', help='AWS configuration profile name') +@click.option('--diff/--no-diff', default=True, help='Print what values were changed in the task definition') +def run(cluster, task, count, command, env, region, access_key_id, secret_access_key, profile, diff): """ Run a one-off task. @@ -235,7 +187,7 @@ def run(cluster, task, count, command, env, region, access_key_id, click.secho('- %s' % started_task['taskArn'], fg='green') click.secho(' ') - except Exception as e: + except EcsError as e: click.secho('%s\n' % str(e), fg='red', err=True) exit(1) @@ -249,7 +201,6 @@ def wait_for_finish(action, timeout, title, success_message, failure_message, inspected_until = None while waiting and datetime.now() < waiting_timeout: click.secho('.', nl=False) - sleep(1) service = action.get_service() inspected_until = inspect_errors( service=service, @@ -260,6 +211,9 @@ def wait_for_finish(action, timeout, title, success_message, failure_message, ) waiting = not action.is_deployed(service) + if waiting: + sleep(1) + inspect_errors( service=service, failure_message=failure_message, @@ -271,6 +225,87 @@ def wait_for_finish(action, timeout, title, success_message, failure_message, click.secho('\n%s\n' % success_message, fg='green') +def deploy_task_definition(deployment, task_definition, title, success_message, + failure_message, timeout, deregister, + previous_task_definition, ignore_warnings): + click.secho('Updating service') + deployment.deploy(task_definition) + + message = 'Successfully changed task definition to: %s:%s\n' % ( + task_definition.family, + task_definition.revision + ) + + click.secho(message, fg='green') + + wait_for_finish( + action=deployment, + timeout=timeout, + title=title, + success_message=success_message, + failure_message=failure_message, + ignore_warnings=ignore_warnings + ) + + if deregister: + deregister_task_definition(deployment, previous_task_definition) + + +def get_task_definition(action, task): + if task: + task_definition = action.get_task_definition(task) + else: + task_definition = action.get_current_task_definition(action.service) + task = task_definition.family_revision + + click.secho('Deploying based on task definition: %s\n' % task) + + return task_definition + + +def create_task_definition(action, task_definition): + click.secho('Creating new task definition revision') + new_td = action.update_task_definition(task_definition) + + click.secho( + 'Successfully created revision: %d\n' % new_td.revision, + fg='green' + ) + + return new_td + + +def deregister_task_definition(action, task_definition): + click.secho('Deregister task definition revision') + action.deregister_task_definition(task_definition) + click.secho( + 'Successfully deregistered revision: %d\n' % task_definition.revision, + fg='green' + ) + + +def rollback_task_definition(deployment, old, new, timeout=600): + click.secho( + 'Rolling back to task definition: %s\n' % old.family_revision, + fg='yellow', + ) + deploy_task_definition( + deployment=deployment, + task_definition=old, + title='Deploying previous task definition', + success_message='Rollback successful', + failure_message='Rollback failed. Please check ECS Console', + timeout=timeout, + deregister=True, + previous_task_definition=new, + ignore_warnings=False + ) + click.secho( + 'Deployment failed, but service has been rolled back to previous ' + 'task definition: %s\n' % old.family_revision, fg='yellow', err=True + ) + + def record_deployment(revision, api_key, app_id, comment, user): api_key = getenv('NEW_RELIC_API_KEY', api_key) app_id = getenv('NEW_RELIC_APP_ID', app_id) @@ -334,7 +369,9 @@ def inspect_errors(service, failure_message, ignore_warnings, since, timeout): if timeout: error = True - failure_message += ' (timeout)' + failure_message += ' due to timeout. Please see: ' \ + 'https://github.com/fabfuel/ecs-deploy#timeout' + click.secho('') if error: raise TaskPlacementError(failure_message) diff --git a/ecs_deploy/ecs.py b/ecs_deploy/ecs.py index 6b47673..2bbc368 100644 --- a/ecs_deploy/ecs.py +++ b/ecs_deploy/ecs.py @@ -5,6 +5,9 @@ from dateutil.tz.tz import tzlocal +VERSION = '1.4.0' + + class EcsClient(object): def __init__(self, access_key_id=None, secret_access_key=None, region=None, profile=None): @@ -375,9 +378,11 @@ def update_task_definition(self, task_definition): additional_properties=task_definition.additional_properties ) new_task_definition = EcsTaskDefinition(**response[u'taskDefinition']) - self._client.deregister_task_definition(task_definition.arn) return new_task_definition + def deregister_task_definition(self, task_definition): + self._client.deregister_task_definition(task_definition.arn) + def update_service(self, service): response = self._client.update_service( cluster=service.cluster, @@ -434,14 +439,20 @@ def service_name(self): class DeployAction(EcsAction): def deploy(self, task_definition): - self._service.set_task_definition(task_definition) - return self.update_service(self._service) + try: + self._service.set_task_definition(task_definition) + return self.update_service(self._service) + except ClientError as e: + raise EcsError(str(e)) class ScaleAction(EcsAction): def scale(self, desired_count): - self._service.set_desired_count(desired_count) - return self.update_service(self._service) + try: + self._service.set_desired_count(desired_count) + return self.update_service(self._service) + except ClientError as e: + raise EcsError(str(e)) class RunAction(EcsAction): @@ -452,15 +463,18 @@ def __init__(self, client, cluster_name): self.started_tasks = [] def run(self, task_definition, count, started_by): - result = self._client.run_task( - cluster=self._cluster_name, - task_definition=task_definition.family_revision, - count=count, - started_by=started_by, - overrides=dict(containerOverrides=task_definition.get_overrides()) - ) - self.started_tasks = result['tasks'] - return True + try: + result = self._client.run_task( + cluster=self._cluster_name, + task_definition=task_definition.family_revision, + count=count, + started_by=started_by, + overrides=dict(containerOverrides=task_definition.get_overrides()) + ) + self.started_tasks = result['tasks'] + return True + except ClientError as e: + raise EcsError(str(e)) class EcsError(Exception): diff --git a/setup.cfg b/setup.cfg index 5f04a8c..cc54d9f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,7 @@ [wheel] universal = 1 -[pytest] +[tool:pytest] testpaths = tests flake8-max-line-length = 120 diff --git a/setup.py b/setup.py index e1c1d4e..d6dec7b 100644 --- a/setup.py +++ b/setup.py @@ -3,13 +3,15 @@ """ from setuptools import find_packages, setup +from ecs_deploy.ecs import VERSION + dependencies = ['click', 'botocore', 'boto3>=1.4.7', 'future', 'requests'] setup( name='ecs-deploy', - version='1.3.1', + version=VERSION, url='https://github.com/fabfuel/ecs-deploy', - download_url='https://github.com/fabfuel/ecs-deploy/archive/1.3.1.tar.gz', + download_url='https://github.com/fabfuel/ecs-deploy/archive/%s.tar.gz' % VERSION, license='BSD', author='Fabian Fuelling', author_email='pypi@fabfuel.de', @@ -33,7 +35,6 @@ 'coverage' ], classifiers=[ - 'Development Status :: 3 - Alpha', 'Environment :: Console', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', diff --git a/tests/test_cli.py b/tests/test_cli.py index f576319..b619871 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -63,6 +63,7 @@ def test_deploy(get_client, runner): result = runner.invoke(cli.deploy, (CLUSTER_NAME, SERVICE_NAME)) assert result.exit_code == 0 assert not result.exception + assert u"Deploying based on task definition: test-task:1" in result.output assert u'Successfully created revision: 2' in result.output assert u'Successfully deregistered revision: 1' in result.output assert u'Successfully changed task definition to: test-task:2' in result.output @@ -70,12 +71,45 @@ def test_deploy(get_client, runner): assert u"Updating task definition" not in result.output +@patch('ecs_deploy.cli.get_client') +def test_deploy_with_rollback(get_client, runner): + get_client.return_value = EcsTestClient('acces_key', 'secret_key', wait=2) + result = runner.invoke(cli.deploy, (CLUSTER_NAME, SERVICE_NAME, '--timeout=1', '--rollback')) + + assert result.exit_code == 1 + assert result.exception + assert u"Deploying based on task definition: test-task:1" in result.output + + assert u"Deployment failed" in result.output + assert u"Rolling back to task definition: test-task:1" in result.output + assert u'Successfully changed task definition to: test-task:1' in result.output + + assert u"Rollback successful" in result.output + assert u'Deployment failed, but service has been rolled back to ' \ + u'previous task definition: test-task:1' in result.output + + +@patch('ecs_deploy.cli.get_client') +def test_deploy_without_deregister(get_client, runner): + get_client.return_value = EcsTestClient('acces_key', 'secret_key') + result = runner.invoke(cli.deploy, (CLUSTER_NAME, SERVICE_NAME, '--no-deregister')) + assert result.exit_code == 0 + assert not result.exception + assert u"Deploying based on task definition: test-task:1" in result.output + assert u'Successfully created revision: 2' in result.output + assert u'Successfully deregistered revision: 1' not in result.output + assert u'Successfully changed task definition to: test-task:2' in result.output + assert u'Deployment successful' in result.output + assert u"Updating task definition" not in result.output + + @patch('ecs_deploy.cli.get_client') def test_deploy_with_role_arn(get_client, runner): get_client.return_value = EcsTestClient('acces_key', 'secret_key') result = runner.invoke(cli.deploy, (CLUSTER_NAME, SERVICE_NAME, '-r', 'arn:new:role')) assert result.exit_code == 0 assert not result.exception + assert u"Deploying based on task definition: test-task:1" in result.output assert u'Successfully created revision: 2' in result.output assert u'Successfully deregistered revision: 1' in result.output assert u'Successfully changed task definition to: test-task:2' in result.output @@ -90,6 +124,7 @@ def test_deploy_new_tag(get_client, runner): result = runner.invoke(cli.deploy, (CLUSTER_NAME, SERVICE_NAME, '-t', 'latest')) assert result.exit_code == 0 assert not result.exception + assert u"Deploying based on task definition: test-task:1" in result.output assert u"Updating task definition" in result.output assert u'Changed image of container "webserver" to: "webserver:latest" (was: "webserver:123")' in result.output assert u'Changed image of container "application" to: "application:latest" (was: "application:123")' in result.output @@ -105,6 +140,7 @@ def test_deploy_one_new_image(get_client, runner): result = runner.invoke(cli.deploy, (CLUSTER_NAME, SERVICE_NAME, '-i', 'application', 'application:latest')) assert result.exit_code == 0 assert not result.exception + assert u"Deploying based on task definition: test-task:1" in result.output assert u"Updating task definition" in result.output assert u'Changed image of container "application" to: "application:latest" (was: "application:123")' in result.output assert u'Successfully created revision: 2' in result.output @@ -120,6 +156,7 @@ def test_deploy_two_new_images(get_client, runner): '-i', 'webserver', 'webserver:latest')) assert result.exit_code == 0 assert not result.exception + assert u"Deploying based on task definition: test-task:1" in result.output assert u"Updating task definition" in result.output assert u'Changed image of container "webserver" to: "webserver:latest" (was: "webserver:123")' in result.output assert u'Changed image of container "application" to: "application:latest" (was: "application:123")' in result.output @@ -135,6 +172,7 @@ def test_deploy_one_new_command(get_client, runner): result = runner.invoke(cli.deploy, (CLUSTER_NAME, SERVICE_NAME, '-c', 'application', 'foobar')) assert result.exit_code == 0 assert not result.exception + assert u"Deploying based on task definition: test-task:1" in result.output assert u"Updating task definition" in result.output assert u'Changed command of container "application" to: "foobar" (was: "run")' in result.output assert u'Successfully created revision: 2' in result.output @@ -153,6 +191,7 @@ def test_deploy_one_new_environment_variable(get_client, runner): assert result.exit_code == 0 assert not result.exception + assert u"Deploying based on task definition: test-task:1" in result.output assert u"Updating task definition" in result.output assert u'Changed environment "foo" of container "application" to: "bar"' in result.output assert u'Changed environment "foo" of container "webserver" to: "baz"' in result.output @@ -171,6 +210,7 @@ def test_deploy_without_changing_environment_value(get_client, runner): assert result.exit_code == 0 assert not result.exception + assert u"Deploying based on task definition: test-task:1" in result.output assert u"Updating task definition" not in result.output assert u'Changed environment' not in result.output assert u'Successfully created revision: 2' in result.output @@ -187,6 +227,7 @@ def test_deploy_without_diff(get_client, runner): assert result.exit_code == 0 assert not result.exception + assert u"Deploying based on task definition: test-task:1" in result.output assert u"Updating task definition" not in result.output assert u'Changed environment' not in result.output assert u'Successfully created revision: 2' in result.output @@ -197,20 +238,29 @@ def test_deploy_without_diff(get_client, runner): @patch('ecs_deploy.cli.get_client') def test_deploy_with_errors(get_client, runner): - get_client.return_value = EcsTestClient('acces_key', 'secret_key', errors=True) + get_client.return_value = EcsTestClient('acces_key', 'secret_key', deployment_errors=True) result = runner.invoke(cli.deploy, (CLUSTER_NAME, SERVICE_NAME)) assert result.exit_code == 1 assert u"Deployment failed" in result.output assert u"ERROR: Service was unable to Lorem Ipsum" in result.output +@patch('ecs_deploy.cli.get_client') +def test_deploy_with_client_errors(get_client, runner): + get_client.return_value = EcsTestClient('acces_key', 'secret_key', client_errors=True) + result = runner.invoke(cli.deploy, (CLUSTER_NAME, SERVICE_NAME)) + assert result.exit_code == 1 + assert u"Something went wrong" in result.output + + @patch('ecs_deploy.cli.get_client') def test_deploy_ignore_warnings(get_client, runner): - get_client.return_value = EcsTestClient('acces_key', 'secret_key', errors=True) + get_client.return_value = EcsTestClient('acces_key', 'secret_key', deployment_errors=True) result = runner.invoke(cli.deploy, (CLUSTER_NAME, SERVICE_NAME, '--ignore-warnings')) assert result.exit_code == 0 assert not result.exception + assert u"Deploying based on task definition: test-task:1" in result.output assert u'Successfully created revision: 2' in result.output assert u'Successfully deregistered revision: 1' in result.output assert u'Successfully changed task definition to: test-task:2' in result.output @@ -219,6 +269,31 @@ def test_deploy_ignore_warnings(get_client, runner): assert u'Deployment successful' in result.output +@patch('ecs_deploy.newrelic.Deployment.deploy') +@patch('ecs_deploy.cli.get_client') +def test_deploy_with_newrelic(get_client, newrelic, runner): + get_client.return_value = EcsTestClient('acces_key', 'secret_key') + result = runner.invoke(cli.deploy, (CLUSTER_NAME, SERVICE_NAME, + '-t', 'my-tag', + '--newrelic-apikey', 'test', + '--newrelic-appid', 'test', + '--comment', 'Lorem Ipsum')) + assert result.exit_code == 0 + assert not result.exception + assert u"Deploying based on task definition: test-task:1" in result.output + assert u'Successfully created revision: 2' in result.output + assert u'Successfully deregistered revision: 1' in result.output + assert u'Successfully changed task definition to: test-task:2' in result.output + assert u'Deployment successful' in result.output + assert u"Recording deployment in New Relic" in result.output + + newrelic.assert_called_once_with( + 'my-tag', + '', + 'Lorem Ipsum' + ) + + @patch('ecs_deploy.newrelic.Deployment.deploy') @patch('ecs_deploy.cli.get_client') def test_deploy_with_newrelic_errors(get_client, deploy, runner): @@ -248,6 +323,15 @@ def test_deploy_task_definition_arn(get_client, runner): assert u'Deployment successful' in result.output +@patch('ecs_deploy.cli.get_client') +def test_deploy_with_timeout(get_client, runner): + get_client.return_value = EcsTestClient('acces_key', 'secret_key', wait=2) + result = runner.invoke(cli.deploy, (CLUSTER_NAME, SERVICE_NAME, '--timeout', '1')) + assert result.exit_code == 1 + assert u"Deployment failed due to timeout. Please see: " \ + u"https://github.com/fabfuel/ecs-deploy#timeout" in result.output + + @patch('ecs_deploy.cli.get_client') def test_deploy_unknown_task_definition_arn(get_client, runner): get_client.return_value = EcsTestClient('acces_key', 'secret_key') @@ -293,16 +377,24 @@ def test_scale(get_client, runner): @patch('ecs_deploy.cli.get_client') def test_scale_with_errors(get_client, runner): - get_client.return_value = EcsTestClient('acces_key', 'secret_key', errors=True) + get_client.return_value = EcsTestClient('acces_key', 'secret_key', deployment_errors=True) result = runner.invoke(cli.scale, (CLUSTER_NAME, SERVICE_NAME, '2')) assert result.exit_code == 1 assert u"Scaling failed" in result.output assert u"ERROR: Service was unable to Lorem Ipsum" in result.output +@patch('ecs_deploy.cli.get_client') +def test_scale_with_client_errors(get_client, runner): + get_client.return_value = EcsTestClient('acces_key', 'secret_key', client_errors=True) + result = runner.invoke(cli.scale, (CLUSTER_NAME, SERVICE_NAME, '2')) + assert result.exit_code == 1 + assert u"Something went wrong" in result.output + + @patch('ecs_deploy.cli.get_client') def test_scale_ignore_warnings(get_client, runner): - get_client.return_value = EcsTestClient('acces_key', 'secret_key', errors=True) + get_client.return_value = EcsTestClient('acces_key', 'secret_key', deployment_errors=True) result = runner.invoke(cli.scale, (CLUSTER_NAME, SERVICE_NAME, '2', '--ignore-warnings')) assert not result.exception @@ -318,7 +410,8 @@ def test_scale_with_timeout(get_client, runner): get_client.return_value = EcsTestClient('acces_key', 'secret_key', wait=2) result = runner.invoke(cli.scale, (CLUSTER_NAME, SERVICE_NAME, '2', '--timeout', '1')) assert result.exit_code == 1 - assert u"Scaling failed (timeout)" in result.output + assert u"Scaling failed due to timeout. Please see: " \ + u"https://github.com/fabfuel/ecs-deploy#timeout" in result.output @patch('ecs_deploy.cli.get_client') @@ -389,8 +482,9 @@ def test_run_task_without_diff(get_client, runner): @patch('ecs_deploy.cli.get_client') def test_run_task_with_errors(get_client, runner): - get_client.return_value = EcsTestClient('acces_key', 'secret_key', errors=True) + get_client.return_value = EcsTestClient('acces_key', 'secret_key', deployment_errors=True) result = runner.invoke(cli.run, (CLUSTER_NAME, 'test-task')) + assert result.exception assert result.exit_code == 1 assert u"An error occurred (123) when calling the fake_error operation: Something went wrong" in result.output diff --git a/tests/test_ecs.py b/tests/test_ecs.py index eeb8e41..b8466b8 100644 --- a/tests/test_ecs.py +++ b/tests/test_ecs.py @@ -586,6 +586,13 @@ def test_update_task_definition(client, task_definition): u'unknownProperty': u'lorem-ipsum' } ) + + +@patch.object(EcsClient, '__init__') +def test_deregister_task_definition(client, task_definition): + action = EcsAction(client, u'test-cluster', u'test-service') + action.deregister_task_definition(task_definition) + client.deregister_task_definition.assert_called_once_with( task_definition.arn ) @@ -733,13 +740,16 @@ def test_run_action_run(client, task_definition): class EcsTestClient(object): - def __init__(self, access_key_id=None, secret_access_key=None, region=None, profile=None, errors=False, wait=0): + def __init__(self, access_key_id=None, secret_access_key=None, region=None, + profile=None, deployment_errors=False, client_errors=False, + wait=0): super(EcsTestClient, self).__init__() self.access_key_id = access_key_id self.secret_access_key = secret_access_key self.region = region self.profile = profile - self.errors = errors + self.deployment_errors = deployment_errors + self.client_errors = client_errors self.wait_until = datetime.now() + timedelta(seconds=wait) def describe_services(self, cluster_name, service_name): @@ -750,7 +760,7 @@ def describe_services(self, cluster_name, service_name): raise ClientError(error_response, u'DescribeServices') if service_name != u'test-service': return {u'services': []} - if self.errors: + if self.deployment_errors: return { u"services": [PAYLOAD_SERVICE_WITH_ERRORS], u"failures": [] @@ -780,7 +790,10 @@ def deregister_task_definition(self, task_definition_arn): return deepcopy(RESPONSE_TASK_DEFINITION) def update_service(self, cluster, service, desired_count, task_definition): - if self.errors: + if self.client_errors: + error = dict(Error=dict(Code=123, Message="Something went wrong")) + raise ClientError(error, 'fake_error') + if self.deployment_errors: return deepcopy(RESPONSE_SERVICE_WITH_ERRORS) return deepcopy(RESPONSE_SERVICE) @@ -789,7 +802,7 @@ def run_task(self, cluster, task_definition, count, started_by, overrides): raise EcsConnectionError(u'Unable to locate credentials. Configure credentials by running "aws configure".') if cluster == 'unknown-cluster': raise EcsConnectionError(u'An error occurred (ClusterNotFoundException) when calling the RunTask operation: Cluster not found.') - if self.errors: + if self.deployment_errors: error = dict(Error=dict(Code=123, Message="Something went wrong")) raise ClientError(error, 'fake_error') return dict(tasks=[dict(taskArn='arn:foo:bar'), dict(taskArn='arn:lorem:ipsum')]) diff --git a/tox.ini b/tox.ini index cd59364..bafa81a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist=py27, py35, flake8 +envlist=py27, py35, py36, flake8 [testenv:py27] basepython = python2.7 @@ -7,6 +7,9 @@ basepython = python2.7 [testenv:py35] basepython = python3.5 +[testenv:py36] +basepython = python3.6 + [testenv] commands=py.test --cov ecs_deploy {posargs} deps= @@ -15,6 +18,7 @@ deps= pytest-mock mock requests + boto3 [testenv:flake8] basepython = python2.7