Completed
Pull Request — master (#39)
by Fabian
49s
created

scale()   B

Complexity

Conditions 2

Size

Total Lines 40

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 2

Importance

Changes 5
Bugs 0 Features 0
Metric Value
cc 2
c 5
b 0
f 0
dl 0
loc 40
ccs 20
cts 20
cp 1
crap 2
rs 8.8571
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, VERSION
12 1
from ecs_deploy.newrelic import Deployment, NewRelicException
13
14
15 1
@click.group()
16 1
@click.version_option(version=VERSION, prog_name='ecs-deploy')
17
def ecs():  # pragma: no cover
18
    pass
19
20
21 1
def get_client(access_key_id, secret_access_key, region, profile):
22 1
    return EcsClient(access_key_id, secret_access_key, region, profile)
23
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('service')
106 1
@click.argument('desired_count', type=int)
107 1
@click.option('--region', help='AWS region (e.g. eu-central-1)')
108 1
@click.option('--access-key-id', help='AWS access key id')
109 1
@click.option('--secret-access-key', help='AWS secret access key')
110 1
@click.option('--profile', help='AWS configuration profile name')
111 1
@click.option('--timeout', default=300, type=int, help='AWS configuration profile')
112 1
@click.option('--ignore-warnings', is_flag=True, help='Do not fail deployment on warnings (port already in use or insufficient memory/CPU)')
113
def scale(cluster, service, desired_count, access_key_id, secret_access_key, region, profile, timeout, ignore_warnings):
114
    """
115
    Scale a service up or down.
116
117
    \b
118
    CLUSTER is the name of your cluster (e.g. 'my-custer') within ECS.
119
    SERVICE is the name of your service (e.g. 'my-app') within ECS.
120
    DESIRED_COUNT is the number of tasks your service should run.
121
    """
122 1
    try:
123 1
        client = get_client(access_key_id, secret_access_key, region, profile)
124 1
        scaling = ScaleAction(client, cluster, service)
125 1
        click.secho('Updating service')
126 1
        scaling.scale(desired_count)
127 1
        click.secho(
128
            'Successfully changed desired count to: %s\n' % desired_count,
129
            fg='green'
130
        )
131 1
        wait_for_finish(
132
            action=scaling,
133
            timeout=timeout,
134
            title='Scaling service',
135
            success_message='Scaling successful',
136
            failure_message='Scaling failed',
137
            ignore_warnings=ignore_warnings
138
        )
139
140 1
    except EcsError as e:
141 1
        click.secho('%s\n' % str(e), fg='red', err=True)
142 1
        exit(1)
143
144
145 1
@click.command()
146 1
@click.argument('cluster')
147 1
@click.argument('task')
148 1
@click.argument('count', required=False, default=1)
149 1
@click.option('-c', '--command', type=(str, str), multiple=True, help='Overwrites the command in a container: <container> <command>')
150 1
@click.option('-e', '--env', type=(str, str, str), multiple=True, help='Adds or changes an environment variable: <container> <name> <value>')
151 1
@click.option('--region', help='AWS region (e.g. eu-central-1)')
152 1
@click.option('--access-key-id', help='AWS access key id')
153 1
@click.option('--secret-access-key', help='AWS secret access key')
154 1
@click.option('--profile', help='AWS configuration profile name')
155 1
@click.option('--diff/--no-diff', default=True, help='Print what values were changed in the task definition')
156
def run(cluster, task, count, command, env, region, access_key_id, secret_access_key, profile, diff):
157
    """
158
    Run a one-off task.
159
160
    \b
161
    CLUSTER is the name of your cluster (e.g. 'my-custer') within ECS.
162
    TASK is the name of your task definition (e.g. 'my-task') within ECS.
163
    COMMAND is the number of tasks your service should run.
164
    """
165 1
    try:
166 1
        client = get_client(access_key_id, secret_access_key, region, profile)
167 1
        action = RunAction(client, cluster)
168
169 1
        td = action.get_task_definition(task)
170 1
        td.set_commands(**{key: value for (key, value) in command})
171 1
        td.set_environment(env)
172
173 1
        if diff:
174 1
            print_diff(td, 'Using task definition: %s' % task)
175
176 1
        action.run(td, count, 'ECS Deploy')
177
178 1
        click.secho(
179
            'Successfully started %d instances of task: %s' % (
180
                len(action.started_tasks),
181
                td.family_revision
182
            ),
183
            fg='green'
184
        )
185
186 1
        for started_task in action.started_tasks:
187 1
            click.secho('- %s' % started_task['taskArn'], fg='green')
188 1
        click.secho(' ')
189
190 1
    except EcsError as e:
191 1
        click.secho('%s\n' % str(e), fg='red', err=True)
192 1
        exit(1)
193
194
195 1
def wait_for_finish(action, timeout, title, success_message, failure_message,
196
                    ignore_warnings):
197 1
    click.secho(title, nl=False)
198 1
    waiting = True
199 1
    waiting_timeout = datetime.now() + timedelta(seconds=timeout)
200 1
    service = action.get_service()
201 1
    inspected_until = None
202 1
    while waiting and datetime.now() < waiting_timeout:
203 1
        click.secho('.', nl=False)
204 1
        service = action.get_service()
205 1
        inspected_until = inspect_errors(
206
            service=service,
207
            failure_message=failure_message,
208
            ignore_warnings=ignore_warnings,
209
            since=inspected_until,
210
            timeout=False
211
        )
212 1
        waiting = not action.is_deployed(service)
213
214 1
        if waiting:
215 1
            sleep(1)
216
217 1
    inspect_errors(
218
        service=service,
219
        failure_message=failure_message,
220
        ignore_warnings=ignore_warnings,
221
        since=inspected_until,
222
        timeout=waiting
223
    )
224
225 1
    click.secho('\n%s\n' % success_message, fg='green')
226
227
228 1
def deploy_task_definition(deployment, task_definition, title, success_message,
229
                           failure_message, timeout, deregister,
230
                           previous_task_definition, ignore_warnings):
231 1
    click.secho('Updating service')
232 1
    deployment.deploy(task_definition)
233
234 1
    message = 'Successfully changed task definition to: %s:%s\n' % (
235
        task_definition.family,
236
        task_definition.revision
237
    )
238
239 1
    click.secho(message, fg='green')
240
241 1
    wait_for_finish(
242
        action=deployment,
243
        timeout=timeout,
244
        title=title,
245
        success_message=success_message,
246
        failure_message=failure_message,
247
        ignore_warnings=ignore_warnings
248
    )
249
250 1
    if deregister:
251 1
        deregister_task_definition(deployment, previous_task_definition)
252
253
254 1
def get_task_definition(action, task):
255 1
    if task:
256 1
        task_definition = action.get_task_definition(task)
257
    else:
258 1
        task_definition = action.get_current_task_definition(action.service)
259 1
        task = task_definition.family_revision
260
261 1
    click.secho('Deploying based on task definition: %s\n' % task)
262
263 1
    return task_definition
264
265
266 1
def create_task_definition(action, task_definition):
267 1
    click.secho('Creating new task definition revision')
268 1
    new_td = action.update_task_definition(task_definition)
269
270 1
    click.secho(
271
        'Successfully created revision: %d\n' % new_td.revision,
272
        fg='green'
273
    )
274
275 1
    return new_td
276
277
278 1
def deregister_task_definition(action, task_definition):
279 1
    click.secho('Deregister task definition revision')
280 1
    action.deregister_task_definition(task_definition)
281 1
    click.secho(
282
        'Successfully deregistered revision: %d\n' % task_definition.revision,
283
        fg='green'
284
    )
285
286
287 1
def rollback_task_definition(deployment, old, new, timeout=600):
288 1
    click.secho(
289
        'Rolling back to task definition: %s\n' % old.family_revision,
290
        fg='yellow',
291
    )
292 1
    deploy_task_definition(
293
        deployment=deployment,
294
        task_definition=old,
295
        title='Deploying previous task definition',
296
        success_message='Rollback successful',
297
        failure_message='Rollback failed. Please check ECS Console',
298
        timeout=timeout,
299
        deregister=True,
300
        previous_task_definition=new,
301
        ignore_warnings=False
302
    )
303 1
    click.secho(
304
        'Deployment failed, but service has been rolled back to previous '
305
        'task definition: %s\n' % old.family_revision, fg='yellow', err=True
306
    )
307
308
309 1
def record_deployment(revision, api_key, app_id, comment, user):
310 1
    api_key = getenv('NEW_RELIC_API_KEY', api_key)
311 1
    app_id = getenv('NEW_RELIC_APP_ID', app_id)
312
313 1
    if not revision or not api_key or not app_id:
314 1
        return False
315
316 1
    user = user or getpass.getuser()
317
318 1
    click.secho('Recording deployment in New Relic', nl=False)
319
320 1
    deployment = Deployment(api_key, app_id, user)
321 1
    deployment.deploy(revision, '', comment)
322
323 1
    click.secho('\nDone\n', fg='green')
324
325 1
    return True
326
327
328 1
def print_diff(task_definition, title='Updating task definition'):
329 1
    if task_definition.diff:
330 1
        click.secho(title)
331 1
        for diff in task_definition.diff:
332 1
            click.secho(str(diff), fg='blue')
333 1
        click.secho('')
334
335
336 1
def inspect_errors(service, failure_message, ignore_warnings, since, timeout):
337 1
    error = False
338 1
    last_error_timestamp = since
339
340 1
    warnings = service.get_warnings(since)
341 1
    for timestamp in warnings:
342 1
        message = warnings[timestamp]
343 1
        click.secho('')
344 1
        if ignore_warnings:
345 1
            last_error_timestamp = timestamp
346 1
            click.secho(
347
                text='%s\nWARNING: %s' % (timestamp, message),
348
                fg='yellow',
349
                err=False
350
            )
351 1
            click.secho('Continuing.', nl=False)
352
        else:
353 1
            click.secho(
354
                text='%s\nERROR: %s\n' % (timestamp, message),
355
                fg='red',
356
                err=True
357
            )
358 1
            error = True
359
360 1
    if service.older_errors:
361 1
        click.secho('')
362 1
        click.secho('Older errors', fg='yellow', err=True)
363 1
        for timestamp in service.older_errors:
364 1
            click.secho(
365
                text='%s\n%s\n' % (timestamp, service.older_errors[timestamp]),
366
                fg='yellow',
367
                err=True
368
            )
369
370 1
    if timeout:
371 1
        error = True
372 1
        failure_message += ' due to timeout. Please see: ' \
373
                           'https://github.com/fabfuel/ecs-deploy#timeout'
374 1
        click.secho('')
375
376 1
    if error:
377 1
        raise TaskPlacementError(failure_message)
378
379 1
    return last_error_timestamp
380
381
382 1
ecs.add_command(deploy)
383 1
ecs.add_command(scale)
384 1
ecs.add_command(run)
385
386
if __name__ == '__main__':  # pragma: no cover
387
    ecs()
388