Passed
Branch develop (de6256)
by Fabian
01:15
created

ecs_deploy.cli.deploy()   B

Complexity

Conditions 5

Size

Total Lines 76
Code Lines 55

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 45
CRAP Score 5

Importance

Changes 0
Metric Value
eloc 55
dl 0
loc 76
rs 8.006
c 0
b 0
f 0
ccs 45
cts 45
cp 1
cc 5
nop 21
crap 5

How to fix   Long Method    Many Parameters   

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:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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