Completed
Pull Request — develop (#58)
by
unknown
02:45
created

cron()   B

Complexity

Conditions 5

Size

Total Lines 58

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 7.0706

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 58
ccs 22
cts 39
cp 0.5641
rs 7.9078
cc 5
crap 7.0706

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 cron(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 definition, and update the `rule` to use the new task definition.
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
    RULE is the name of the rule to use the new task definition.
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
        target = client.events.list_targets_by_rule(Rule=rule)['Targets'][0]
149
        client.update_rule(
150
            rule=rule,
151
            arn=new_td.arn.partition('task-definition')[0] + 'cluster/' + cluster,
152
            target_id=target['Id'],
153
            role_arn=target['RoleArn'],
154
            task_definition_arn=new_td.arn
155
        )
156
        click.secho('Updated scheduled task %s' % new_td.arn)
157
158
    except EcsError as e:
159
        click.secho('%s\n' % str(e), fg='red', err=True)
160
        exit(1)
161
162
163 1
@click.command()
164 1
@click.argument('cluster')
165 1
@click.argument('service')
166 1
@click.argument('desired_count', type=int)
167 1
@click.option('--region', help='AWS region (e.g. eu-central-1)')
168 1
@click.option('--access-key-id', help='AWS access key id')
169 1
@click.option('--secret-access-key', help='AWS secret access key')
170 1
@click.option('--profile', help='AWS configuration profile name')
171 1
@click.option('--timeout', default=300, type=int, help='AWS configuration profile')
172 1
@click.option('--ignore-warnings', is_flag=True, help='Do not fail deployment on warnings (port already in use or insufficient memory/CPU)')
173
def scale(cluster, service, desired_count, access_key_id, secret_access_key, region, profile, timeout, ignore_warnings):
174
    """
175
    Scale a service up or down.
176
177
    \b
178
    CLUSTER is the name of your cluster (e.g. 'my-custer') within ECS.
179
    SERVICE is the name of your service (e.g. 'my-app') within ECS.
180
    DESIRED_COUNT is the number of tasks your service should run.
181
    """
182 1
    try:
183 1
        client = get_client(access_key_id, secret_access_key, region, profile)
184 1
        scaling = ScaleAction(client, cluster, service)
185 1
        click.secho('Updating service')
186 1
        scaling.scale(desired_count)
187 1
        click.secho(
188
            'Successfully changed desired count to: %s\n' % desired_count,
189
            fg='green'
190
        )
191 1
        wait_for_finish(
192
            action=scaling,
193
            timeout=timeout,
194
            title='Scaling service',
195
            success_message='Scaling successful',
196
            failure_message='Scaling failed',
197
            ignore_warnings=ignore_warnings
198
        )
199
200 1
    except EcsError as e:
201 1
        click.secho('%s\n' % str(e), fg='red', err=True)
202 1
        exit(1)
203
204
205 1
@click.command()
206 1
@click.argument('cluster')
207 1
@click.argument('task')
208 1
@click.argument('count', required=False, default=1)
209 1
@click.option('-c', '--command', type=(str, str), multiple=True, help='Overwrites the command in a container: <container> <command>')
210 1
@click.option('-e', '--env', type=(str, str, str), multiple=True, help='Adds or changes an environment variable: <container> <name> <value>')
211 1
@click.option('--region', help='AWS region (e.g. eu-central-1)')
212 1
@click.option('--access-key-id', help='AWS access key id')
213 1
@click.option('--secret-access-key', help='AWS secret access key')
214 1
@click.option('--profile', help='AWS configuration profile name')
215 1
@click.option('--diff/--no-diff', default=True, help='Print what values were changed in the task definition')
216
def run(cluster, task, count, command, env, region, access_key_id, secret_access_key, profile, diff):
217
    """
218
    Run a one-off task.
219
220
    \b
221
    CLUSTER is the name of your cluster (e.g. 'my-custer') within ECS.
222
    TASK is the name of your task definition (e.g. 'my-task') within ECS.
223
    COMMAND is the number of tasks your service should run.
224
    """
225 1
    try:
226 1
        client = get_client(access_key_id, secret_access_key, region, profile)
227 1
        action = RunAction(client, cluster)
228
229 1
        td = action.get_task_definition(task)
230 1
        td.set_commands(**{key: value for (key, value) in command})
231 1
        td.set_environment(env)
232
233 1
        if diff:
234 1
            print_diff(td, 'Using task definition: %s' % task)
235
236 1
        action.run(td, count, 'ECS Deploy')
237
238 1
        click.secho(
239
            'Successfully started %d instances of task: %s' % (
240
                len(action.started_tasks),
241
                td.family_revision
242
            ),
243
            fg='green'
244
        )
245
246 1
        for started_task in action.started_tasks:
247 1
            click.secho('- %s' % started_task['taskArn'], fg='green')
248 1
        click.secho(' ')
249
250 1
    except EcsError as e:
251 1
        click.secho('%s\n' % str(e), fg='red', err=True)
252 1
        exit(1)
253
254
255 1
def wait_for_finish(action, timeout, title, success_message, failure_message,
256
                    ignore_warnings):
257 1
    click.secho(title, nl=False)
258 1
    waiting = True
259 1
    waiting_timeout = datetime.now() + timedelta(seconds=timeout)
260 1
    service = action.get_service()
261 1
    inspected_until = None
262 1
    while waiting and datetime.now() < waiting_timeout:
263 1
        click.secho('.', nl=False)
264 1
        service = action.get_service()
265 1
        inspected_until = inspect_errors(
266
            service=service,
267
            failure_message=failure_message,
268
            ignore_warnings=ignore_warnings,
269
            since=inspected_until,
270
            timeout=False
271
        )
272 1
        waiting = not action.is_deployed(service)
273
274 1
        if waiting:
275 1
            sleep(1)
276
277 1
    inspect_errors(
278
        service=service,
279
        failure_message=failure_message,
280
        ignore_warnings=ignore_warnings,
281
        since=inspected_until,
282
        timeout=waiting
283
    )
284
285 1
    click.secho('\n%s\n' % success_message, fg='green')
286
287
288 1
def deploy_task_definition(deployment, task_definition, title, success_message,
289
                           failure_message, timeout, deregister,
290
                           previous_task_definition, ignore_warnings):
291 1
    click.secho('Updating service')
292 1
    deployment.deploy(task_definition)
293
294 1
    message = 'Successfully changed task definition to: %s:%s\n' % (
295
        task_definition.family,
296
        task_definition.revision
297
    )
298
299 1
    click.secho(message, fg='green')
300
301 1
    wait_for_finish(
302
        action=deployment,
303
        timeout=timeout,
304
        title=title,
305
        success_message=success_message,
306
        failure_message=failure_message,
307
        ignore_warnings=ignore_warnings
308
    )
309
310 1
    if deregister:
311 1
        deregister_task_definition(deployment, previous_task_definition)
312
313
314 1
def get_task_definition(action, task):
315 1
    if task:
316 1
        task_definition = action.get_task_definition(task)
317
    else:
318 1
        task_definition = action.get_current_task_definition(action.service)
319 1
        task = task_definition.family_revision
320
321 1
    click.secho('Deploying based on task definition: %s\n' % task)
322
323 1
    return task_definition
324
325
326 1
def create_task_definition(action, task_definition):
327 1
    click.secho('Creating new task definition revision')
328 1
    new_td = action.update_task_definition(task_definition)
329
330 1
    click.secho(
331
        'Successfully created revision: %d\n' % new_td.revision,
332
        fg='green'
333
    )
334
335 1
    return new_td
336
337
338 1
def deregister_task_definition(action, task_definition):
339 1
    click.secho('Deregister task definition revision')
340 1
    action.deregister_task_definition(task_definition)
341 1
    click.secho(
342
        'Successfully deregistered revision: %d\n' % task_definition.revision,
343
        fg='green'
344
    )
345
346
347 1
def rollback_task_definition(deployment, old, new, timeout=600):
348 1
    click.secho(
349
        'Rolling back to task definition: %s\n' % old.family_revision,
350
        fg='yellow',
351
    )
352 1
    deploy_task_definition(
353
        deployment=deployment,
354
        task_definition=old,
355
        title='Deploying previous task definition',
356
        success_message='Rollback successful',
357
        failure_message='Rollback failed. Please check ECS Console',
358
        timeout=timeout,
359
        deregister=True,
360
        previous_task_definition=new,
361
        ignore_warnings=False
362
    )
363 1
    click.secho(
364
        'Deployment failed, but service has been rolled back to previous '
365
        'task definition: %s\n' % old.family_revision, fg='yellow', err=True
366
    )
367
368
369 1
def record_deployment(revision, api_key, app_id, comment, user):
370 1
    api_key = getenv('NEW_RELIC_API_KEY', api_key)
371 1
    app_id = getenv('NEW_RELIC_APP_ID', app_id)
372
373 1
    if not revision or not api_key or not app_id:
374 1
        return False
375
376 1
    user = user or getpass.getuser()
377
378 1
    click.secho('Recording deployment in New Relic', nl=False)
379
380 1
    deployment = Deployment(api_key, app_id, user)
381 1
    deployment.deploy(revision, '', comment)
382
383 1
    click.secho('\nDone\n', fg='green')
384
385 1
    return True
386
387
388 1
def print_diff(task_definition, title='Updating task definition'):
389 1
    if task_definition.diff:
390 1
        click.secho(title)
391 1
        for diff in task_definition.diff:
392 1
            click.secho(str(diff), fg='blue')
393 1
        click.secho('')
394
395
396 1
def inspect_errors(service, failure_message, ignore_warnings, since, timeout):
397 1
    error = False
398 1
    last_error_timestamp = since
399
400 1
    warnings = service.get_warnings(since)
401 1
    for timestamp in warnings:
402 1
        message = warnings[timestamp]
403 1
        click.secho('')
404 1
        if ignore_warnings:
405 1
            last_error_timestamp = timestamp
406 1
            click.secho(
407
                text='%s\nWARNING: %s' % (timestamp, message),
408
                fg='yellow',
409
                err=False
410
            )
411 1
            click.secho('Continuing.', nl=False)
412
        else:
413 1
            click.secho(
414
                text='%s\nERROR: %s\n' % (timestamp, message),
415
                fg='red',
416
                err=True
417
            )
418 1
            error = True
419
420 1
    if service.older_errors:
421 1
        click.secho('')
422 1
        click.secho('Older errors', fg='yellow', err=True)
423 1
        for timestamp in service.older_errors:
424 1
            click.secho(
425
                text='%s\n%s\n' % (timestamp, service.older_errors[timestamp]),
426
                fg='yellow',
427
                err=True
428
            )
429
430 1
    if timeout:
431 1
        error = True
432 1
        failure_message += ' due to timeout. Please see: ' \
433
                           'https://github.com/fabfuel/ecs-deploy#timeout'
434 1
        click.secho('')
435
436 1
    if error:
437 1
        raise TaskPlacementError(failure_message)
438
439 1
    return last_error_timestamp
440
441
442 1
ecs.add_command(deploy)
443 1
ecs.add_command(scale)
444 1
ecs.add_command(run)
445 1
ecs.add_command(cron)
446
447
if __name__ == '__main__':  # pragma: no cover
448
    ecs()
449