Skip to content

Commit e1699f0

Browse files
authored
Merge pull request #246 from Mirochill/fix-240-force-new-deployment
Add force new deployment option
2 parents 935e33b + 9ea0702 commit e1699f0

5 files changed

Lines changed: 90 additions & 18 deletions

File tree

README.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,13 @@ This will duplicate the current task definition and cause the service to redeplo
151151

152152
$ ecs deploy my-cluster my-service
153153

154+
Force a new deployment
155+
======================
156+
To ask ECS to start a new deployment even when the service is already using the requested task definition, pass
157+
``--force-new-deployment``. This is useful when an image tag points to a new digest and you want ECS to resolve it again.::
158+
159+
$ ecs deploy my-cluster my-service --force-new-deployment
160+
154161

155162
Deploy a new tag
156163
================

ecs_deploy/cli.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ def get_client(access_key_id, secret_access_key, region, profile, assume_account
6161
@click.option('--account', help='Target AWS account id to deploy in')
6262
@click.option('--assume-role', help='AWS Role to assume in target account')
6363
@click.option('--timeout', required=False, default=300, type=int, help='Amount of seconds to wait for deployment before command fails (default: 300). To disable timeout (fire and forget) set to -1')
64+
@click.option('--force-new-deployment', is_flag=True, default=False, help='Force ECS to start a new deployment')
6465
@click.option('--ignore-warnings', is_flag=True, help='Do not fail deployment on warnings (port already in use or insufficient memory/CPU)')
6566
@click.option('--newrelic-apikey', required=False, help='New Relic API Key for recording the deployment. Can also be defined via environment variable NEW_RELIC_API_KEY')
6667
@click.option('--newrelic-appid', required=False, help='New Relic App ID for recording the deployment. Can also be defined via environment variable NEW_RELIC_APP_ID')
@@ -85,7 +86,7 @@ def get_client(access_key_id, secret_access_key, region, profile, assume_account
8586
@click.option('--volume', type=(str, str), multiple=True, required=False, help='Set volume mapping from host to container in the task definition.')
8687
@click.option('--add-container', type=str, multiple=True, required=False, help='Add a placeholder container in the task definition.')
8788
@click.option('--remove-container', type=str, multiple=True, required=False, help='Remove a container from the task definition.')
88-
def deploy(cluster, service, tag, image, command, health_check, cpu, memory, memoryreservation, task_cpu, task_memory, privileged, essential, env, env_file, s3_env_file, secret, secrets_env_file, ulimit, system_control, port, mount, log, role, execution_role, runtime_platform, task, region, access_key_id, secret_access_key, profile, account, assume_role, timeout, newrelic_apikey, newrelic_appid, newrelic_region, newrelic_revision, comment, user, ignore_warnings, diff, deregister, rollback, exclusive_env, exclusive_secrets, exclusive_s3_env_file, sleep_time, exclusive_ulimits, exclusive_system_controls, exclusive_ports, exclusive_mounts, volume, add_container, remove_container, slack_url, docker_label, exclusive_docker_labels, slack_service_match='.*'):
89+
def deploy(cluster, service, tag, image, command, health_check, cpu, memory, memoryreservation, task_cpu, task_memory, privileged, essential, env, env_file, s3_env_file, secret, secrets_env_file, ulimit, system_control, port, mount, log, role, execution_role, runtime_platform, task, region, access_key_id, secret_access_key, profile, account, assume_role, timeout, force_new_deployment, newrelic_apikey, newrelic_appid, newrelic_region, newrelic_revision, comment, user, ignore_warnings, diff, deregister, rollback, exclusive_env, exclusive_secrets, exclusive_s3_env_file, sleep_time, exclusive_ulimits, exclusive_system_controls, exclusive_ports, exclusive_mounts, volume, add_container, remove_container, slack_url, docker_label, exclusive_docker_labels, slack_service_match='.*'):
8990
"""
9091
Redeploy or modify a service.
9192
@@ -153,7 +154,8 @@ def deploy(cluster, service, tag, image, command, health_check, cpu, memory, mem
153154
deregister=deregister,
154155
previous_task_definition=td,
155156
ignore_warnings=ignore_warnings,
156-
sleep_time=sleep_time
157+
sleep_time=sleep_time,
158+
force_new_deployment=force_new_deployment
157159
)
158160

159161
except TaskPlacementError as e:
@@ -560,9 +562,13 @@ def wait_for_finish(action, timeout, title, success_message, failure_message,
560562

561563
def deploy_task_definition(deployment, task_definition, title, success_message,
562564
failure_message, timeout, deregister,
563-
previous_task_definition, ignore_warnings, sleep_time):
565+
previous_task_definition, ignore_warnings, sleep_time,
566+
force_new_deployment=False):
564567
click.secho('Updating service')
565-
deployment.deploy(task_definition)
568+
deployment.deploy(
569+
task_definition,
570+
force_new_deployment=force_new_deployment
571+
)
566572

567573
message = 'Successfully changed task definition to: %s:%s\n' % (
568574
task_definition.family,

ecs_deploy/ecs.py

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -142,19 +142,17 @@ def deregister_task_definition(self, task_definition_arn):
142142
taskDefinition=task_definition_arn
143143
)
144144

145-
def update_service(self, cluster, service, desired_count, task_definition):
146-
if desired_count is None:
147-
return self.boto.update_service(
148-
cluster=cluster,
149-
service=service,
150-
taskDefinition=task_definition
151-
)
152-
return self.boto.update_service(
145+
def update_service(self, cluster, service, desired_count, task_definition, force_new_deployment=False):
146+
kwargs = dict(
153147
cluster=cluster,
154148
service=service,
155-
desiredCount=desired_count,
156149
taskDefinition=task_definition
157150
)
151+
if desired_count is not None:
152+
kwargs['desiredCount'] = desired_count
153+
if force_new_deployment:
154+
kwargs['forceNewDeployment'] = True
155+
return self.boto.update_service(**kwargs)
158156

159157
def run_task(self, cluster, task_definition, count, started_by, overrides,
160158
launchtype='EC2', subnets=(), security_groups=(),
@@ -1370,13 +1368,16 @@ def update_task_definition(self, task_definition):
13701368
def deregister_task_definition(self, task_definition):
13711369
self._client.deregister_task_definition(task_definition.arn)
13721370

1373-
def update_service(self, service, desired_count=None):
1374-
response = self._client.update_service(
1371+
def update_service(self, service, desired_count=None, force_new_deployment=False):
1372+
kwargs = dict(
13751373
cluster=service.cluster,
13761374
service=service.name,
13771375
desired_count=desired_count,
13781376
task_definition=service.task_definition
13791377
)
1378+
if force_new_deployment:
1379+
kwargs['force_new_deployment'] = True
1380+
response = self._client.update_service(**kwargs)
13801381
return EcsService(self._cluster_name, response[u'service'])
13811382

13821383
def is_deployed(self, service):
@@ -1431,10 +1432,13 @@ def service_name(self):
14311432

14321433

14331434
class DeployAction(EcsAction):
1434-
def deploy(self, task_definition):
1435+
def deploy(self, task_definition, force_new_deployment=False):
14351436
try:
14361437
self._service.set_task_definition(task_definition)
1437-
return self.update_service(self._service)
1438+
return self.update_service(
1439+
self._service,
1440+
force_new_deployment=force_new_deployment
1441+
)
14381442
except ClientError as e:
14391443
raise EcsError(str(e))
14401444

tests/test_cli.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,15 @@ def test_deploy_without_deregister(get_client, runner):
115115
assert u"Updating task definition" not in result.output
116116

117117

118+
@patch('ecs_deploy.cli.get_client')
119+
def test_deploy_with_force_new_deployment(get_client, runner):
120+
get_client.return_value = EcsTestClient('acces_key', 'secret_key')
121+
result = runner.invoke(cli.deploy, (CLUSTER_NAME, SERVICE_NAME, '--force-new-deployment'))
122+
assert result.exit_code == 0
123+
assert not result.exception
124+
assert get_client.return_value.force_new_deployment is True
125+
126+
118127
@patch('ecs_deploy.cli.get_client')
119128
def test_deploy_with_role_arn(get_client, runner):
120129
get_client.return_value = EcsTestClient('acces_key', 'secret_key')

tests/test_ecs.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1420,6 +1420,16 @@ def test_client_update_service_without_desired_count(client):
14201420
)
14211421

14221422

1423+
def test_client_update_service_with_force_new_deployment(client):
1424+
client.update_service(u'test-cluster', u'test-service', None, u'task-definition', True)
1425+
client.boto.update_service.assert_called_once_with(
1426+
cluster=u'test-cluster',
1427+
service=u'test-service',
1428+
taskDefinition=u'task-definition',
1429+
forceNewDeployment=True
1430+
)
1431+
1432+
14231433
def test_client_run_task(client):
14241434
client.run_task(
14251435
cluster=u'test-cluster',
@@ -1545,6 +1555,23 @@ def test_update_service(client, service):
15451555
)
15461556

15471557

1558+
@patch.object(EcsClient, '__init__')
1559+
def test_update_service_with_force_new_deployment(client, service):
1560+
client.update_service.return_value = RESPONSE_SERVICE
1561+
1562+
action = EcsAction(client, CLUSTER_NAME, SERVICE_NAME)
1563+
new_service = action.update_service(service, force_new_deployment=True)
1564+
1565+
assert isinstance(new_service, EcsService)
1566+
client.update_service.assert_called_once_with(
1567+
cluster=service.cluster,
1568+
service=service.name,
1569+
desired_count=None,
1570+
task_definition=service.task_definition,
1571+
force_new_deployment=True
1572+
)
1573+
1574+
15481575
@patch.object(EcsClient, '__init__')
15491576
def test_is_deployed(client, service):
15501577
client.list_tasks.return_value = RESPONSE_LIST_TASKS_1
@@ -1644,6 +1671,23 @@ def test_deploy_action(client, task_definition_revision_2):
16441671
)
16451672

16461673

1674+
@patch.object(EcsClient, '__init__')
1675+
def test_deploy_action_with_force_new_deployment(client, task_definition_revision_2):
1676+
action = DeployAction(client, CLUSTER_NAME, SERVICE_NAME)
1677+
updated_service = action.deploy(task_definition_revision_2, force_new_deployment=True)
1678+
1679+
assert action.service.task_definition == task_definition_revision_2.arn
1680+
assert isinstance(updated_service, EcsService)
1681+
1682+
client.update_service.assert_called_once_with(
1683+
cluster=action.service.cluster,
1684+
service=action.service.name,
1685+
desired_count=action.service.desired_count,
1686+
task_definition=task_definition_revision_2.arn,
1687+
force_new_deployment=True
1688+
)
1689+
1690+
16471691
@patch.object(EcsClient, '__init__')
16481692
def test_scale_action(client):
16491693
action = ScaleAction(client, CLUSTER_NAME, SERVICE_NAME)
@@ -1792,6 +1836,7 @@ def __init__(self, access_key_id=None, secret_access_key=None, region=None,
17921836
self.deployment_errors = deployment_errors
17931837
self.client_errors = client_errors
17941838
self.wait_until = datetime.now() + timedelta(seconds=wait)
1839+
self.force_new_deployment = False
17951840

17961841
def describe_services(self, cluster_name, service_name):
17971842
if not self.access_key_id or not self.secret_access_key:
@@ -1835,7 +1880,8 @@ def register_task_definition(self, family, containers, volumes, role_arn,
18351880
def deregister_task_definition(self, task_definition_arn):
18361881
return deepcopy(RESPONSE_TASK_DEFINITION)
18371882

1838-
def update_service(self, cluster, service, desired_count, task_definition):
1883+
def update_service(self, cluster, service, desired_count, task_definition, force_new_deployment=False):
1884+
self.force_new_deployment = force_new_deployment
18391885
if self.client_errors:
18401886
error = dict(Error=dict(Code=123, Message="Something went wrong"))
18411887
raise ClientError(error, 'fake_error')

0 commit comments

Comments
 (0)