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

create_task_definition()   A

Complexity

Conditions 1

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
c 1
b 0
f 0
dl 0
loc 10
ccs 5
cts 5
cp 1
crap 1
rs 9.4285
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(deployment, new_td)
76
77 1
            wait_for_finish(
78
                action=deployment,
79
                timeout=timeout,
80
                title='Deploying new task definition',
81
                success_message='Deployment successful',
82
                failure_message='Deployment failed',
83
                ignore_warnings=ignore_warnings
84
            )
85
86 1
        except TaskPlacementError as e:
87 1
            if rollback:
88 1
                click.secho('%s\n' % str(e), fg='red', err=True)
89 1
                rollback_task_definition(deployment, td, new_td)
90 1
                exit(1)
91
            else:
92 1
                raise
93
94 1
        if deregister:
95 1
            deregister_task_definition(deployment, td)
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):
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
241 1
def get_task_definition(action, task):
242 1
    if task:
243 1
        task_definition = action.get_task_definition(task)
244
    else:
245 1
        task_definition = action.get_current_task_definition(action.service)
246 1
        task = task_definition.family_revision
247
248 1
    click.secho('Deploying based on task definition: %s\n' % task)
249
250 1
    return task_definition
251
252
253 1
def create_task_definition(action, task_definition):
254 1
    click.secho('Creating new task definition revision')
255 1
    new_td = action.update_task_definition(task_definition)
256
257 1
    click.secho(
258
        'Successfully created revision: %d\n' % new_td.revision,
259
        fg='green'
260
    )
261
262 1
    return new_td
263
264
265 1
def deregister_task_definition(action, task_definition):
266 1
    click.secho('Deregister task definition revision')
267 1
    action.deregister_task_definition(task_definition)
268 1
    click.secho(
269
        'Successfully deregistered revision: %d\n' % task_definition.revision,
270
        fg='green'
271
    )
272
273
274 1
def rollback_task_definition(deployment, old, new, timeout=600):
275 1
    click.secho(
276
        'Rolling back to task definition: %s\n' % old.family_revision,
277
        fg='yellow',
278
    )
279 1
    deploy_task_definition(deployment, old)
280 1
    wait_for_finish(
281
        action=deployment,
282
        timeout=timeout,
283
        title='Deploying previous task definition',
284
        success_message='Rollback successful',
285
        failure_message='Rollback failed. Please check ECS Console',
286
        ignore_warnings=False
287
    )
288 1
    deregister_task_definition(deployment, new)
289
290 1
    click.secho(
291
        'Deployment failed, but service has been rolled back to previous '
292
        'task definition: %s\n' % old.family_revision, fg='yellow', err=True
293
    )
294
295
296 1
def record_deployment(revision, api_key, app_id, comment, user):
297 1
    api_key = getenv('NEW_RELIC_API_KEY', api_key)
298 1
    app_id = getenv('NEW_RELIC_APP_ID', app_id)
299
300 1
    if not revision or not api_key or not app_id:
301 1
        return False
302
303 1
    user = user or getpass.getuser()
304
305 1
    click.secho('Recording deployment in New Relic', nl=False)
306
307 1
    deployment = Deployment(api_key, app_id, user)
308 1
    deployment.deploy(revision, '', comment)
309
310 1
    click.secho('\nDone\n', fg='green')
311
312 1
    return True
313
314
315 1
def print_diff(task_definition, title='Updating task definition'):
316 1
    if task_definition.diff:
317 1
        click.secho(title)
318 1
        for diff in task_definition.diff:
319 1
            click.secho(str(diff), fg='blue')
320 1
        click.secho('')
321
322
323 1
def inspect_errors(service, failure_message, ignore_warnings, since, timeout):
324 1
    error = False
325 1
    last_error_timestamp = since
326
327 1
    warnings = service.get_warnings(since)
328 1
    for timestamp in warnings:
329 1
        message = warnings[timestamp]
330 1
        click.secho('')
331 1
        if ignore_warnings:
332 1
            last_error_timestamp = timestamp
333 1
            click.secho(
334
                text='%s\nWARNING: %s' % (timestamp, message),
335
                fg='yellow',
336
                err=False
337
            )
338 1
            click.secho('Continuing.', nl=False)
339
        else:
340 1
            click.secho(
341
                text='%s\nERROR: %s\n' % (timestamp, message),
342
                fg='red',
343
                err=True
344
            )
345 1
            error = True
346
347 1
    if service.older_errors:
348 1
        click.secho('')
349 1
        click.secho('Older errors', fg='yellow', err=True)
350 1
        for timestamp in service.older_errors:
351 1
            click.secho(
352
                text='%s\n%s\n' % (timestamp, service.older_errors[timestamp]),
353
                fg='yellow',
354
                err=True
355
            )
356
357 1
    if timeout:
358 1
        error = True
359 1
        failure_message += ' due to timeout. Please see: ' \
360
                           'https://github.com/fabfuel/ecs-deploy#timeout'
361 1
        click.secho('')
362
363 1
    if error:
364 1
        raise TaskPlacementError(failure_message)
365
366 1
    return last_error_timestamp
367
368
369 1
ecs.add_command(deploy)
370 1
ecs.add_command(scale)
371 1
ecs.add_command(run)
372
373
if __name__ == '__main__':  # pragma: no cover
374
    ecs()
375