Completed
Branch feature/rollback (6131ac)
by Fabian
29s
created

rollback_task_definition()   A

Complexity

Conditions 1

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
c 0
b 0
f 0
dl 0
loc 19
ccs 6
cts 6
cp 1
crap 1
rs 9.4285
1 1
from __future__ import print_function, absolute_import
2
3 1
from os import getenv
4 1
from time import sleep
5
6 1
import click
7 1
import getpass
8 1
from datetime import datetime, timedelta
9
10 1
from ecs_deploy.ecs import DeployAction, ScaleAction, RunAction, EcsClient, \
11
    TaskPlacementError, EcsError
12 1
from ecs_deploy.newrelic import Deployment, NewRelicException
13
14
15 1
@click.group()
16
def ecs():  # pragma: no cover
17
    pass
18
19
20 1
def get_client(access_key_id, secret_access_key, region, profile):
21 1
    return EcsClient(access_key_id, secret_access_key, region, profile)
22
23
24 1
@click.command()
25 1
@click.argument('cluster')
26 1
@click.argument('service')
27 1
@click.option('-t', '--tag',
28
              help='Changes the tag for ALL container images')
29 1
@click.option('-i', '--image', type=(str, str), multiple=True,
30
              help='Overwrites the image for a container: '
31
                   '<container> <image>')
32 1
@click.option('-c', '--command', type=(str, str), multiple=True,
33
              help='Overwrites the command in a container: '
34
                   '<container> <command>')
35 1
@click.option('-e', '--env', type=(str, str, str), multiple=True,
36
              help='Adds or changes an environment variable: '
37
                   '<container> <name> <value>')
38 1
@click.option('-r', '--role', type=str,
39
              help='Sets the task\'s role ARN: <task role ARN>')
40 1
@click.option('--task', type=str,
41
              help='Task definition to be deployed. Can be a task ARN '
42
                   'or a task family with optional revision')
43 1
@click.option('--region', required=False,
44
              help='AWS region (e.g. eu-central-1)')
45 1
@click.option('--access-key-id', required=False,
46
              help='AWS access key id')
47 1
@click.option('--secret-access-key', required=False,
48
              help='AWS secret access key')
49 1
@click.option('--profile', required=False,
50
              help='AWS configuration profile name')
51 1
@click.option('--timeout', required=False, default=300, type=int,
52
              help='Amount of seconds to wait for deployment before '
53
                   'command fails (default: 300)')
54 1
@click.option('--ignore-warnings', is_flag=True,
55
              help='Do not fail deployment on warnings (port already in use '
56
                   'or insufficient memory/CPU)')
57 1
@click.option('--newrelic-apikey', required=False,
58
              help='New Relic API Key for recording the deployment')
59 1
@click.option('--newrelic-appid', required=False,
60
              help='New Relic App ID for recording the deployment')
61 1
@click.option('--comment', required=False,
62
              help='Description/comment for recording the deployment')
63 1
@click.option('--user', required=False,
64
              help='User who executes the deployment (used for recording)')
65 1
@click.option('--diff/--no-diff', default=True,
66
              help='Print which values were changed in the task definition '
67
                   '(default: --diff)')
68 1
@click.option('--deregister/--no-deregister', default=True,
69
              help='Deregister or keep the old task definition '
70
                   '(default: --deregister)')
71 1
@click.option('--rollback/--no-rollback', default=False,
72
              help='Rollback to previous revision, if deployment failed '
73
                   '(default: --rollback)')
74
def deploy(cluster, service, tag, image, command, env, role, task, region,
75
           access_key_id, secret_access_key, profile, timeout, newrelic_apikey,
76
           newrelic_appid, comment, user, ignore_warnings, diff, deregister,
77
           rollback):
78
    """
79
    Redeploy or modify a service.
80
81
    \b
82
    CLUSTER is the name of your cluster (e.g. 'my-custer') within ECS.
83
    SERVICE is the name of your service (e.g. 'my-app') within ECS.
84
85
    When not giving any other options, the task definition will not be changed.
86
    It will just be duplicated, so that all container images will be pulled
87
    and redeployed.
88
    """
89
90 1
    try:
91 1
        client = get_client(access_key_id, secret_access_key, region, profile)
92 1
        deployment = DeployAction(client, cluster, service)
93
94 1
        td = get_task_definition(deployment, task)
95 1
        td.set_images(tag, **{key: value for (key, value) in image})
96 1
        td.set_commands(**{key: value for (key, value) in command})
97 1
        td.set_environment(env)
98 1
        td.set_role_arn(role)
99
100 1
        if diff:
101 1
            print_diff(td)
102
103 1
        new_td = create_task_definition(deployment, td)
104
105 1
        try:
106 1
            deploy_task_definition(deployment, new_td)
107
108 1
            wait_for_finish(
109
                action=deployment,
110
                timeout=timeout,
111
                title='Deploying new task definition',
112
                success_message='Deployment successful',
113
                failure_message='Deployment failed',
114
                ignore_warnings=ignore_warnings
115
            )
116
117 1
        except TaskPlacementError as e:
118 1
            if rollback:
119 1
                click.secho('%s\n' % str(e), fg='red', err=True)
120 1
                rollback_task_definition(deployment, td, new_td)
121 1
                exit(1)
122
            else:
123 1
                raise
124
125 1
        if deregister:
126 1
            deregister_task_definition(deployment, td)
127
128 1
        record_deployment(tag, newrelic_apikey, newrelic_appid, comment, user)
129
130 1
    except (EcsError, NewRelicException) as e:
131 1
        click.secho('%s\n' % str(e), fg='red', err=True)
132 1
        exit(1)
133
134
135 1
@click.command()
136 1
@click.argument('cluster')
137 1
@click.argument('service')
138 1
@click.argument('desired_count', type=int)
139 1
@click.option('--region',
140
              help='AWS region (e.g. eu-central-1)')
141 1
@click.option('--access-key-id',
142
              help='AWS access key id')
143 1
@click.option('--secret-access-key',
144
              help='AWS secret access key')
145 1
@click.option('--profile',
146
              help='AWS configuration profile name')
147 1
@click.option('--timeout', default=300, type=int,
148
              help='AWS configuration profile')
149 1
@click.option('--ignore-warnings', is_flag=True,
150
              help='Do not fail deployment on warnings (port already in use '
151
                   'or insufficient memory/CPU)')
152
def scale(cluster, service, desired_count, access_key_id, secret_access_key,
153
          region, profile, timeout, ignore_warnings):
154
    """
155
    Scale a service up or down.
156
157
    \b
158
    CLUSTER is the name of your cluster (e.g. 'my-custer') within ECS.
159
    SERVICE is the name of your service (e.g. 'my-app') within ECS.
160
    DESIRED_COUNT is the number of tasks your service should run.
161
    """
162 1
    try:
163 1
        client = get_client(access_key_id, secret_access_key, region, profile)
164 1
        scaling = ScaleAction(client, cluster, service)
165 1
        click.secho('Updating service')
166 1
        scaling.scale(desired_count)
167 1
        click.secho(
168
            'Successfully changed desired count to: %s\n' % desired_count,
169
            fg='green'
170
        )
171 1
        wait_for_finish(
172
            action=scaling,
173
            timeout=timeout,
174
            title='Scaling service',
175
            success_message='Scaling successful',
176
            failure_message='Scaling failed',
177
            ignore_warnings=ignore_warnings
178
        )
179
180 1
    except EcsError as e:
181 1
        click.secho('%s\n' % str(e), fg='red', err=True)
182 1
        exit(1)
183
184
185 1
@click.command()
186 1
@click.argument('cluster')
187 1
@click.argument('task')
188 1
@click.argument('count', required=False, default=1)
189 1
@click.option('-c', '--command', type=(str, str), multiple=True,
190
              help='Overwrites the command in a container: '
191
                   '<container> <command>')
192 1
@click.option('-e', '--env', type=(str, str, str), multiple=True,
193
              help='Adds or changes an environment variable: '
194
                   '<container> <name> <value>')
195 1
@click.option('--region',
196
              help='AWS region (e.g. eu-central-1)')
197 1
@click.option('--access-key-id',
198
              help='AWS access key id')
199 1
@click.option('--secret-access-key',
200
              help='AWS secret access key')
201 1
@click.option('--profile',
202
              help='AWS configuration profile name')
203 1
@click.option('--diff/--no-diff', default=True,
204
              help='Print what values were changed in the task definition')
205
def run(cluster, task, count, command, env, region, access_key_id,
206
        secret_access_key, profile, diff):
207
    """
208
    Run a one-off task.
209
210
    \b
211
    CLUSTER is the name of your cluster (e.g. 'my-custer') within ECS.
212
    TASK is the name of your task definition (e.g. 'my-task') within ECS.
213
    COMMAND is the number of tasks your service should run.
214
    """
215 1
    try:
216 1
        client = get_client(access_key_id, secret_access_key, region, profile)
217 1
        action = RunAction(client, cluster)
218
219 1
        td = action.get_task_definition(task)
220 1
        td.set_commands(**{key: value for (key, value) in command})
221 1
        td.set_environment(env)
222
223 1
        if diff:
224 1
            print_diff(td, 'Using task definition: %s' % task)
225
226 1
        action.run(td, count, 'ECS Deploy')
227
228 1
        click.secho(
229
            'Successfully started %d instances of task: %s' % (
230
                len(action.started_tasks),
231
                td.family_revision
232
            ),
233
            fg='green'
234
        )
235
236 1
        for started_task in action.started_tasks:
237 1
            click.secho('- %s' % started_task['taskArn'], fg='green')
238 1
        click.secho(' ')
239
240 1
    except EcsError as e:
241 1
        click.secho('%s\n' % str(e), fg='red', err=True)
242 1
        exit(1)
243
244
245 1
def wait_for_finish(action, timeout, title, success_message, failure_message,
246
                    ignore_warnings):
247 1
    click.secho(title, nl=False)
248 1
    waiting = True
249 1
    waiting_timeout = datetime.now() + timedelta(seconds=timeout)
250 1
    service = action.get_service()
251 1
    inspected_until = None
252 1
    while waiting and datetime.now() < waiting_timeout:
253 1
        click.secho('.', nl=False)
254 1
        service = action.get_service()
255 1
        inspected_until = inspect_errors(
256
            service=service,
257
            failure_message=failure_message,
258
            ignore_warnings=ignore_warnings,
259
            since=inspected_until,
260
            timeout=False
261
        )
262 1
        waiting = not action.is_deployed(service)
263
264 1
        if waiting:
265 1
            sleep(1)
266
267 1
    inspect_errors(
268
        service=service,
269
        failure_message=failure_message,
270
        ignore_warnings=ignore_warnings,
271
        since=inspected_until,
272
        timeout=waiting
273
    )
274
275 1
    click.secho('\n%s\n' % success_message, fg='green')
276
277
278 1
def deploy_task_definition(deployment, task_definition):
279 1
    click.secho('Updating service')
280 1
    deployment.deploy(task_definition)
281
282 1
    message = 'Successfully changed task definition to: %s:%s\n' % (
283
        task_definition.family,
284
        task_definition.revision
285
    )
286
287 1
    click.secho(message, fg='green')
288
289
290 1
def get_task_definition(action, task):
291 1
    if task:
292 1
        task_definition = action.get_task_definition(task)
293
    else:
294 1
        task_definition = action.get_current_task_definition(action.service)
295 1
        task = task_definition.family_revision
296
297 1
    click.secho('Deploying based on task definition: %s\n' % task)
298
299 1
    return task_definition
300
301
302 1
def create_task_definition(action, task_definition):
303 1
    click.secho('Creating new task definition revision')
304 1
    new_td = action.update_task_definition(task_definition)
305
306 1
    click.secho(
307
        'Successfully created revision: %d\n' % new_td.revision,
308
        fg='green'
309
    )
310
311 1
    return new_td
312
313
314 1
def deregister_task_definition(action, task_definition):
315 1
    click.secho('Deregister task definition revision')
316 1
    action.deregister_task_definition(task_definition)
317 1
    click.secho(
318
        'Successfully deregistered revision: %d\n' % task_definition.revision,
319
        fg='green'
320
    )
321
322
323 1
def rollback_task_definition(deployment, old, new, timeout=600):
324 1
    click.secho(
325
        'Rolling back to task definition: %s\n' % old.family_revision,
326
        fg='yellow',
327
    )
328 1
    deploy_task_definition(deployment, old)
329 1
    wait_for_finish(
330
        action=deployment,
331
        timeout=timeout,
332
        title='Deploying previous task definition',
333
        success_message='Rollback successful',
334
        failure_message='Rollback failed. Please check ECS Console',
335
        ignore_warnings=False
336
    )
337 1
    deregister_task_definition(deployment, new)
338
339 1
    click.secho(
340
        'Deployment failed, but service has been rolled back to previous '
341
        'task definition: %s\n' % old.family_revision, fg='yellow', err=True
342
    )
343
344
345 1
def record_deployment(revision, api_key, app_id, comment, user):
346 1
    api_key = getenv('NEW_RELIC_API_KEY', api_key)
347 1
    app_id = getenv('NEW_RELIC_APP_ID', app_id)
348
349 1
    if not revision or not api_key or not app_id:
350 1
        return False
351
352 1
    user = user or getpass.getuser()
353
354 1
    click.secho('Recording deployment in New Relic', nl=False)
355
356 1
    deployment = Deployment(api_key, app_id, user)
357 1
    deployment.deploy(revision, '', comment)
358
359 1
    click.secho('\nDone\n', fg='green')
360
361 1
    return True
362
363
364 1
def print_diff(task_definition, title='Updating task definition'):
365 1
    if task_definition.diff:
366 1
        click.secho(title)
367 1
        for diff in task_definition.diff:
368 1
            click.secho(str(diff), fg='blue')
369 1
        click.secho('')
370
371
372 1
def inspect_errors(service, failure_message, ignore_warnings, since, timeout):
373 1
    error = False
374 1
    last_error_timestamp = since
375
376 1
    warnings = service.get_warnings(since)
377 1
    for timestamp in warnings:
378 1
        message = warnings[timestamp]
379 1
        click.secho('')
380 1
        if ignore_warnings:
381 1
            last_error_timestamp = timestamp
382 1
            click.secho(
383
                text='%s\nWARNING: %s' % (timestamp, message),
384
                fg='yellow',
385
                err=False
386
            )
387 1
            click.secho('Continuing.', nl=False)
388
        else:
389 1
            click.secho(
390
                text='%s\nERROR: %s\n' % (timestamp, message),
391
                fg='red',
392
                err=True
393
            )
394 1
            error = True
395
396 1
    if service.older_errors:
397 1
        click.secho('')
398 1
        click.secho('Older errors', fg='yellow', err=True)
399 1
        for timestamp in service.older_errors:
400 1
            click.secho(
401
                text='%s\n%s\n' % (timestamp, service.older_errors[timestamp]),
402
                fg='yellow',
403
                err=True
404
            )
405
406 1
    if timeout:
407 1
        error = True
408 1
        failure_message += ' due to timeout. Please see: ' \
409
                           'https://github.com/fabfuel/ecs-deploy#timeout'
410 1
        click.secho('')
411
412 1
    if error:
413 1
        raise TaskPlacementError(failure_message)
414
415 1
    return last_error_timestamp
416
417
418 1
ecs.add_command(deploy)
419 1
ecs.add_command(scale)
420 1
ecs.add_command(run)
421
422
if __name__ == '__main__':  # pragma: no cover
423
    ecs()
424