Completed
Pull Request — develop (#34)
by Fabian
58s
created

deploy_task_definition()   B

Complexity

Conditions 2

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 2

Importance

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