Skip to content

Commit 3089261

Browse files
committed
Merge branch 'release/1.10.0'
2 parents c247184 + c679ae8 commit 3089261

11 files changed

Lines changed: 620 additions & 45 deletions

File tree

ecs_deploy/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
VERSION = '1.9.0'
1+
VERSION = '1.10.0'

ecs_deploy/cli.py

Lines changed: 84 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44
from time import sleep
55

66
import click
7+
import json
78
import getpass
89
from datetime import datetime, timedelta
910

1011
from ecs_deploy import VERSION
11-
from ecs_deploy.ecs import DeployAction, ScaleAction, RunAction, EcsClient, \
12+
from ecs_deploy.ecs import DeployAction, ScaleAction, RunAction, EcsClient, DiffAction, \
1213
TaskPlacementError, EcsError, UpdateAction, LAUNCH_TYPE_EC2, LAUNCH_TYPE_FARGATE
1314
from ecs_deploy.newrelic import Deployment, NewRelicException
15+
from ecs_deploy.slack import SlackNotification
1416

1517

1618
@click.group()
@@ -40,8 +42,9 @@ def get_client(access_key_id, secret_access_key, region, profile):
4042
@click.option('--profile', required=False, help='AWS configuration profile name')
4143
@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')
4244
@click.option('--ignore-warnings', is_flag=True, help='Do not fail deployment on warnings (port already in use or insufficient memory/CPU)')
43-
@click.option('--newrelic-apikey', required=False, help='New Relic API Key for recording the deployment')
44-
@click.option('--newrelic-appid', required=False, help='New Relic App ID for recording the deployment')
45+
@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')
46+
@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')
47+
@click.option('--newrelic-region', required=False, help='New Relic region: US or EU (default: US). Can also be defined via environment variable NEW_RELIC_REGION')
4548
@click.option('--comment', required=False, help='Description/comment for recording the deployment')
4649
@click.option('--user', required=False, help='User who executes the deployment (used for recording)')
4750
@click.option('--diff/--no-diff', default=True, help='Print which values were changed in the task definition (default: --diff)')
@@ -50,7 +53,9 @@ def get_client(access_key_id, secret_access_key, region, profile):
5053
@click.option('--exclusive-env', is_flag=True, default=False, help='Set the given environment variables exclusively and remove all other pre-existing env variables from all containers')
5154
@click.option('--exclusive-secrets', is_flag=True, default=False, help='Set the given secrets exclusively and remove all other pre-existing secrets from all containers')
5255
@click.option('--sleep-time', default=1, type=int, help='Amount of seconds to wait between each check of the service (default: 1)')
53-
def deploy(cluster, service, tag, image, command, env, secret, role, execution_role, task, region, access_key_id, secret_access_key, profile, timeout, newrelic_apikey, newrelic_appid, comment, user, ignore_warnings, diff, deregister, rollback, exclusive_env, exclusive_secrets, sleep_time):
56+
@click.option('--slack-url', required=False, help='Webhook URL of the Slack integration. Can also be defined via environment variable SLACK_URL')
57+
@click.option('--slack-service-match', default=".*", required=False, help='A regular expression for defining, which services should be notified. (default: .* =all). Can also be defined via environment variable SLACK_SERVICE_MATCH')
58+
def deploy(cluster, service, tag, image, command, env, secret, role, execution_role, task, region, access_key_id, secret_access_key, profile, timeout, newrelic_apikey, newrelic_appid, newrelic_region, comment, user, ignore_warnings, diff, deregister, rollback, exclusive_env, exclusive_secrets, sleep_time, slack_url, slack_service_match='.*'):
5459
"""
5560
Redeploy or modify a service.
5661
@@ -75,6 +80,12 @@ def deploy(cluster, service, tag, image, command, env, secret, role, execution_r
7580
td.set_role_arn(role)
7681
td.set_execution_role_arn(execution_role)
7782

83+
slack = SlackNotification(
84+
getenv('SLACK_URL', slack_url),
85+
getenv('SLACK_SERVICE_MATCH', slack_service_match)
86+
)
87+
slack.notify_start(cluster, tag, td, comment, user, service=service)
88+
7889
click.secho('Deploying based on task definition: %s\n' % td.family_revision)
7990

8091
if diff:
@@ -97,14 +108,17 @@ def deploy(cluster, service, tag, image, command, env, secret, role, execution_r
97108
)
98109

99110
except TaskPlacementError as e:
111+
slack.notify_failure(cluster, str(e), service=service)
100112
if rollback:
101113
click.secho('%s\n' % str(e), fg='red', err=True)
102114
rollback_task_definition(deployment, td, new_td, sleep_time=sleep_time)
103115
exit(1)
104116
else:
105117
raise
106118

107-
record_deployment(tag, newrelic_apikey, newrelic_appid, comment, user)
119+
record_deployment(tag, newrelic_apikey, newrelic_appid, newrelic_region, comment, user)
120+
121+
slack.notify_success(cluster, td.revision, service=service)
108122

109123
except (EcsError, NewRelicException) as e:
110124
click.secho('%s\n' % str(e), fg='red', err=True)
@@ -123,15 +137,18 @@ def deploy(cluster, service, tag, image, command, env, secret, role, execution_r
123137
@click.option('--region', help='AWS region (e.g. eu-central-1)')
124138
@click.option('--access-key-id', help='AWS access key id')
125139
@click.option('--secret-access-key', help='AWS secret access key')
126-
@click.option('--newrelic-apikey', required=False, help='New Relic API Key for recording the deployment')
127-
@click.option('--newrelic-appid', required=False, help='New Relic App ID for recording the deployment')
140+
@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')
141+
@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')
142+
@click.option('--newrelic-region', required=False, help='New Relic region: US or EU (default: US). Can also be defined via environment variable NEW_RELIC_REGION')
128143
@click.option('--comment', required=False, help='Description/comment for recording the deployment')
129144
@click.option('--user', required=False, help='User who executes the deployment (used for recording)')
130145
@click.option('--profile', help='AWS configuration profile name')
131146
@click.option('--diff/--no-diff', default=True, help='Print what values were changed in the task definition')
132147
@click.option('--deregister/--no-deregister', default=True, help='Deregister or keep the old task definition (default: --deregister)')
133148
@click.option('--rollback/--no-rollback', default=False, help='Rollback to previous revision, if deployment failed (default: --no-rollback)')
134-
def cron(cluster, task, rule, image, tag, command, env, role, region, access_key_id, secret_access_key, newrelic_apikey, newrelic_appid, comment, user, profile, diff, deregister, rollback):
149+
@click.option('--slack-url', required=False, help='Webhook URL of the Slack integration. Can also be defined via environment variable SLACK_URL')
150+
@click.option('--slack-service-match', default=".*", required=False, help='A regular expression for defining, deployments of which crons should be notified. (default: .* =all). Can also be defined via environment variable SLACK_SERVICE_MATCH')
151+
def cron(cluster, task, rule, image, tag, command, env, role, region, access_key_id, secret_access_key, newrelic_apikey, newrelic_appid, newrelic_region, comment, user, profile, diff, deregister, rollback, slack_url, slack_service_match):
135152
"""
136153
Update a scheduled task.
137154
@@ -152,6 +169,12 @@ def cron(cluster, task, rule, image, tag, command, env, role, region, access_key
152169
td.set_environment(env)
153170
td.set_role_arn(role)
154171

172+
slack = SlackNotification(
173+
getenv('SLACK_URL', slack_url),
174+
getenv('SLACK_SERVICE_MATCH', slack_service_match)
175+
)
176+
slack.notify_start(cluster, tag, td, comment, user, rule=rule)
177+
155178
if diff:
156179
print_diff(td)
157180

@@ -165,7 +188,9 @@ def cron(cluster, task, rule, image, tag, command, env, role, region, access_key
165188
click.secho('Updating scheduled task')
166189
click.secho('Successfully updated scheduled task %s\n' % rule, fg='green')
167190

168-
record_deployment(tag, newrelic_apikey, newrelic_appid, comment, user)
191+
slack.notify_success(cluster, td.revision, rule=rule)
192+
193+
record_deployment(tag, newrelic_apikey, newrelic_appid, newrelic_region, comment, user)
169194

170195
if deregister:
171196
deregister_task_definition(action, td)
@@ -324,6 +349,52 @@ def run(cluster, task, count, command, env, secret, launchtype, subnet, security
324349
exit(1)
325350

326351

352+
@click.command()
353+
@click.argument('task')
354+
@click.argument('revision_a')
355+
@click.argument('revision_b')
356+
@click.option('--region', help='AWS region (e.g. eu-central-1)')
357+
@click.option('--access-key-id', help='AWS access key id')
358+
@click.option('--secret-access-key', help='AWS secret access key')
359+
@click.option('--profile', help='AWS configuration profile name')
360+
def diff(task, revision_a, revision_b, region, access_key_id, secret_access_key, profile):
361+
"""
362+
Compare two task definition revisions.
363+
364+
\b
365+
TASK is the name of your task definition (e.g. 'my-task') within ECS.
366+
COUNT is the number of tasks your service should run.
367+
"""
368+
369+
try:
370+
client = get_client(access_key_id, secret_access_key, region, profile)
371+
action = DiffAction(client)
372+
373+
td_a = action.get_task_definition('%s:%s' % (task, revision_a))
374+
td_b = action.get_task_definition('%s:%s' % (task, revision_b))
375+
376+
result = td_a.diff_raw(td_b)
377+
for difference in result:
378+
if difference[0] == 'add':
379+
click.secho('%s: %s' % (difference[0], difference[1]), fg='green')
380+
for added in difference[2]:
381+
click.secho(' + %s: %s' % (added[0], json.dumps(added[1])), fg='green')
382+
383+
if difference[0] == 'change':
384+
click.secho('%s: %s' % (difference[0], difference[1]), fg='yellow')
385+
click.secho(' - %s' % json.dumps(difference[2][0]), fg='red')
386+
click.secho(' + %s' % json.dumps(difference[2][1]), fg='green')
387+
388+
if difference[0] == 'remove':
389+
click.secho('%s: %s' % (difference[0], difference[1]), fg='red')
390+
for removed in difference[2]:
391+
click.secho(' - %s: %s' % removed, fg='red')
392+
393+
except EcsError as e:
394+
click.secho('%s\n' % str(e), fg='red', err=True)
395+
exit(1)
396+
397+
327398
def wait_for_finish(action, timeout, title, success_message, failure_message,
328399
ignore_warnings, sleep_time=1):
329400
click.secho(title, nl=False)
@@ -441,9 +512,10 @@ def rollback_task_definition(deployment, old, new, timeout=600, sleep_time=1):
441512
)
442513

443514

444-
def record_deployment(revision, api_key, app_id, comment, user):
515+
def record_deployment(revision, api_key, app_id, region, comment, user):
445516
api_key = getenv('NEW_RELIC_API_KEY', api_key)
446517
app_id = getenv('NEW_RELIC_APP_ID', app_id)
518+
region = getenv('NEW_RELIC_REGION', region)
447519

448520
if not revision or not api_key or not app_id:
449521
return False
@@ -452,7 +524,7 @@ def record_deployment(revision, api_key, app_id, comment, user):
452524

453525
click.secho('Recording deployment in New Relic', nl=False)
454526

455-
deployment = Deployment(api_key, app_id, user)
527+
deployment = Deployment(api_key, app_id, user, region)
456528
deployment.deploy(revision, '', comment)
457529

458530
click.secho('\nDone\n', fg='green')
@@ -519,6 +591,7 @@ def inspect_errors(service, failure_message, ignore_warnings, since, timeout):
519591
ecs.add_command(run)
520592
ecs.add_command(cron)
521593
ecs.add_command(update)
594+
ecs.add_command(diff)
522595

523596
if __name__ == '__main__': # pragma: no cover
524597
ecs()

ecs_deploy/ecs.py

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from boto3.session import Session
66
from botocore.exceptions import ClientError, NoCredentialsError
77
from dateutil.tz.tz import tzlocal
8+
from dictdiffer import diff
89

910
JSON_LIST_REGEX = re.compile(r'^\[.*\]$')
1011

@@ -124,18 +125,9 @@ def run_task(self, cluster, task_definition, count, started_by, overrides,
124125

125126
def update_rule(self, cluster, rule, task_definition):
126127
target = self.events.list_targets_by_rule(Rule=rule)['Targets'][0]
127-
self.events.put_targets(
128-
Rule=rule,
129-
Targets=[{
130-
'Arn': task_definition.arn.partition('task-definition')[0] + 'cluster/' + cluster,
131-
'Id': target['Id'],
132-
'RoleArn': target['RoleArn'],
133-
'EcsParameters': {
134-
'TaskDefinitionArn': task_definition.arn,
135-
'TaskCount': 1
136-
}
137-
}]
138-
)
128+
target['Arn'] = task_definition.arn.partition('task-definition')[0] + 'cluster/' + cluster
129+
target['EcsParameters']['TaskDefinitionArn'] = task_definition.arn
130+
self.events.put_targets(Rule=rule, Targets=[target])
139131
return target['Id']
140132

141133

@@ -237,6 +229,47 @@ def family_revision(self):
237229
def diff(self):
238230
return self._diff
239231

232+
def diff_raw(self, task_b):
233+
containers_a = {c['name']: c for c in self.containers}
234+
containers_b = {c['name']: c for c in task_b.containers}
235+
236+
requirements_a = sorted([r['name'] for r in self.requires_attributes])
237+
requirements_b = sorted([r['name'] for r in task_b.requires_attributes])
238+
239+
for container in containers_a:
240+
containers_a[container]['environment'] = {e['name']: e['value'] for e in containers_a[container].get('environment', {})}
241+
242+
for container in containers_b:
243+
containers_b[container]['environment'] = {e['name']: e['value'] for e in containers_b[container].get('environment', {})}
244+
245+
for container in containers_a:
246+
containers_a[container]['secrets'] = {e['name']: e['valueFrom'] for e in containers_a[container].get('secrets', {})}
247+
248+
for container in containers_b:
249+
containers_b[container]['secrets'] = {e['name']: e['valueFrom'] for e in containers_b[container].get('secrets', {})}
250+
251+
composite_a = {
252+
'containers': containers_a,
253+
'volumes': self.volumes,
254+
'requires_attributes': requirements_a,
255+
'role_arn': self.role_arn,
256+
'execution_role_arn': self.execution_role_arn,
257+
'compatibilities': self.compatibilities,
258+
'additional_properties': self.additional_properties,
259+
}
260+
261+
composite_b = {
262+
'containers': containers_b,
263+
'volumes': task_b.volumes,
264+
'requires_attributes': requirements_b,
265+
'role_arn': task_b.role_arn,
266+
'execution_role_arn': task_b.execution_role_arn,
267+
'compatibilities': task_b.compatibilities,
268+
'additional_properties': task_b.additional_properties,
269+
}
270+
271+
return list(diff(composite_a, composite_b))
272+
240273
def get_overrides(self):
241274
override = dict()
242275
overrides = []
@@ -670,6 +703,11 @@ def __init__(self, client):
670703
super(UpdateAction, self).__init__(client, None, None)
671704

672705

706+
class DiffAction(EcsAction):
707+
def __init__(self, client):
708+
super(DiffAction, self).__init__(client, None, None)
709+
710+
673711
class EcsError(Exception):
674712
pass
675713

ecs_deploy/newrelic.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,23 @@ class NewRelicDeploymentException(NewRelicException):
1010

1111

1212
class Deployment(object):
13-
ENDPOINT = 'https://api.newrelic.com/v2/applications/%(app_id)s/deployments.json'
13+
API_HOST_US = 'api.newrelic.com'
14+
API_HOST_EU = 'api.eu.newrelic.com'
15+
ENDPOINT = 'https://%(host)s/v2/applications/%(app_id)s/deployments.json'
1416

15-
def __init__(self, api_key, app_id, user):
17+
def __init__(self, api_key, app_id, user, region):
1618
self.__api_key = api_key
1719
self.__app_id = app_id
1820
self.__user = user
21+
self.__region = region.lower() if region else 'us'
1922

2023
@property
2124
def endpoint(self):
22-
return self.ENDPOINT % dict(app_id=self.__app_id)
25+
if self.__region == 'eu':
26+
host = self.API_HOST_EU
27+
else:
28+
host = self.API_HOST_US
29+
return self.ENDPOINT % dict(host=host, app_id=self.__app_id)
2330

2431
@property
2532
def headers(self):

0 commit comments

Comments
 (0)