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

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