Completed
Pull Request — develop (#58)
by
unknown
03:51
created

update_task_and_rule()   B

Complexity

Conditions 5

Size

Total Lines 56

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 6.8667

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 56
ccs 22
cts 38
cp 0.5789
rs 8.4294
cc 5
crap 6.8667

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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